From a1aaf7bc0f543539a8316b4256c1a279e8fd1cd9 Mon Sep 17 00:00:00 2001 From: movabo Date: Sat, 10 Apr 2021 02:03:26 +0200 Subject: [PATCH] [octree] Add (face) neighbor-method and save the index of the cube in the parents array in `Cube::m_child_in_parent` --- include/inexor/vulkan-renderer/world/cube.hpp | 30 +++++-- src/vulkan-renderer/world/cube.cpp | 80 +++++++++++++++++-- tests/CMakeLists.txt | 1 + tests/world/cube.cpp | 30 +++++++ 4 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 tests/world/cube.cpp diff --git a/include/inexor/vulkan-renderer/world/cube.hpp b/include/inexor/vulkan-renderer/world/cube.hpp index 1b5180780..faa8a0fca 100644 --- a/include/inexor/vulkan-renderer/world/cube.hpp +++ b/include/inexor/vulkan-renderer/world/cube.hpp @@ -5,10 +5,14 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include // forward declaration @@ -42,6 +46,8 @@ class Cube : public std::enable_shared_from_this { static constexpr std::size_t EDGES{12}; /// Cube Type. enum class Type { EMPTY = 0b00u, SOLID = 0b01u, NORMAL = 0b10u, OCTANT = 0b11u }; + enum class NeighborAxis { X = 2, Y = 1, Z = 0 }; + enum class NeighborDirection { POSITIVE, NEGATIVE }; /// IDs of the children and edges which will be swapped to receive the rotation. /// To achieve a 90 degree rotation the 0th index have to be swapped with the 1st and the 1st with the 2nd, etc. @@ -58,13 +64,16 @@ class Cube : public std::enable_shared_from_this { }; private: - Type m_type{Type::SOLID}; + Type m_type{Type::EMPTY}; float m_size{32}; glm::vec3 m_position{0.0f, 0.0f, 0.0f}; /// Root cube is empty. std::weak_ptr m_parent{}; + /// Index of this in m_parent.m_children; undefined behavior if root. + std::uint8_t m_index_in_parent{}; + /// Indentations, should only be used if it is a geometry cube. std::array m_indentations; std::array, Cube::SUB_CUBES> m_children; @@ -86,12 +95,12 @@ class Cube : public std::enable_shared_from_this { void rotate(const RotationAxis::Type &axis); public: - /// Create a solid cube. + /// Create an empty cube. Cube() = default; - /// Create a solid cube. + /// Create an empty cube. Cube(float size, const glm::vec3 &position); - /// Create a solid cube. - Cube(std::weak_ptr parent, float size, const glm::vec3 &position); + /// Create an empty cube. + Cube(std::weak_ptr parent, std::uint8_t index, float size, const glm::vec3 &position); /// Use clone() to create an independent copy of a cube. Cube(const Cube &rhs) = delete; Cube(Cube &&rhs) noexcept; @@ -161,6 +170,17 @@ class Cube : public std::enable_shared_from_this { /// Recursive way to collect all the caches. /// @param update_invalid If true it will update invalid polygon caches. [[nodiscard]] std::vector polygons(bool update_invalid = false) const; + + /// Get the (face) neighbor of this cube by using a similar implementation to Samets "OT_GTEQ_FACE_NEIGHBOR(P,I)". + /// @brief Get the (face) neighbor of this cube. + /// @param axis The axis on which to get the neighboring cube + /// @param direction Whether to get the cube which is above or below this cube on the selected axis + /// @returns Same-sized neighbor if existent, else larger neighbor if exists, otherwise (i.e. when no neighbor + /// exists) nullptr. + /// @see Samet, H. (1989) [Neighbor finding in Images Represented by Octrees.] + /// (https://web.archive.org/web/20190712063957/http://www.cs.umd.edu/~hjs/pubs/SameCVGIP89.pdf) + /// Computer Vision, Graphics, and Image Processing. 46 (3), 367-386. + [[nodiscard]] std::shared_ptr neighbor(NeighborAxis axis, NeighborDirection direction); }; } // namespace inexor::vulkan_renderer::world diff --git a/src/vulkan-renderer/world/cube.cpp b/src/vulkan-renderer/world/cube.cpp index b54f99e41..189552460 100644 --- a/src/vulkan-renderer/world/cube.cpp +++ b/src/vulkan-renderer/world/cube.cpp @@ -1,17 +1,12 @@ #include "inexor/vulkan-renderer/world/cube.hpp" #include "inexor/vulkan-renderer/world/indentation.hpp" -#include - -#include -#include -#include - void swap(inexor::vulkan_renderer::world::Cube &lhs, inexor::vulkan_renderer::world::Cube &rhs) noexcept { std::swap(lhs.m_type, rhs.m_type); std::swap(lhs.m_size, rhs.m_size); std::swap(lhs.m_position, rhs.m_position); std::swap(lhs.m_parent, rhs.m_parent); + std::swap(lhs.m_index_in_parent, rhs.m_index_in_parent); std::swap(lhs.m_indentations, rhs.m_indentations); std::swap(lhs.m_children, rhs.m_children); std::swap(lhs.m_polygon_cache, rhs.m_polygon_cache); @@ -176,8 +171,10 @@ void Cube::rotate<3>(const RotationAxis::Type &axis) { Cube::Cube(const float size, const glm::vec3 &position) : m_size(size), m_position(position) {} -Cube::Cube(std::weak_ptr parent, const float size, const glm::vec3 &position) : Cube(size, position) { +Cube::Cube(std::weak_ptr parent, const std::uint8_t index, const float size, const glm::vec3 &position) + : Cube(size, position) { m_parent = std::move(parent); + m_index_in_parent = index; } Cube::Cube(Cube &&rhs) noexcept : Cube() { @@ -202,6 +199,7 @@ std::shared_ptr Cube::operator[](std::size_t idx) const { std::shared_ptr Cube::clone() const { std::shared_ptr clone = std::make_shared(this->m_size, this->m_position); clone->m_type = this->m_type; + clone->m_index_in_parent = this->m_index_in_parent; if (clone->m_type == Type::NORMAL) { clone->m_indentations = this->m_indentations; @@ -259,8 +257,9 @@ void Cube::set_type(const Type new_type) { break; case Type::OCTANT: const float half_size = m_size / 2; + std::uint8_t index = 0; auto create_cube = [&](const glm::vec3 &offset) { - return std::make_shared(weak_from_this(), half_size, m_position + offset); + return std::make_shared(weak_from_this(), index++, half_size, m_position + offset); }; // Look into octree documentation to find information about the order of subcubes in space. // We can't use initializer list here because clang-tidy complains about it. @@ -426,4 +425,69 @@ std::vector Cube::polygons(const bool update_invalid) const { collect(*this); return polygons; } +std::shared_ptr Cube::neighbor(const NeighborAxis axis, const NeighborDirection direction) { + if (is_root()) { + return nullptr; + } + + // Each axis only requires information and manipulation of one (relevant) bit to find the neighbor. + const auto relevant_index_bit = static_cast(axis); // bit index of the axis we are working on + + auto get_bit = [&relevant_index_bit](const std::uint8_t cube_index) { + return ((cube_index >> relevant_index_bit) & 1u) != 0; + }; + auto toggle_bit = [&relevant_index_bit](const std::uint8_t cube_index) { + return cube_index ^ (1u << relevant_index_bit); + }; + + auto parent = m_parent.lock(); + const std::uint8_t index = m_index_in_parent; + const bool home_bit = get_bit(index); + + // The relevant bit denotes whether `m_parent` and `this` share a face on the upper side of the relevant axis. + // If they share one and also the user wants to go in the positive direction, then the neighbor is not a sibling. + // (Same for opposite, i.e. share face on lower side and going negative direction.) + if (home_bit && direction == NeighborDirection::NEGATIVE || !home_bit && direction == NeighborDirection::POSITIVE) { + // The demanded neighbor is a sibling! Return the neighboring sibling. + return parent->m_children[toggle_bit(index)]; + } + if (parent->is_root()) { + return nullptr; + } + // the neighbor is further away than a sibling + + // Keep the history of indices because we just need to mirror indices (i.e. toggle the relevant bit) + // to find the desired neighboring cube. + std::vector history; + history.push_back(index); + + // Find the first cube where the bit (of `get_bit`) is different to `this_bit`. + // That cubes parent is the first mutual parent of the desired neighbor and `this`. + std::uint8_t p_index = parent->m_index_in_parent; + history.push_back(p_index); + while (get_bit(p_index) == home_bit) { + parent = parent->m_parent.lock(); + if (parent->is_root()) { + return nullptr; + } + p_index = parent->m_index_in_parent; + history.push_back(p_index); + } + + // get the first mutual parent of neighbor and `this`. + std::shared_ptr child = parent->m_parent.lock(); + + // Now mirror the path we took by just flipping the relevant bit of each index in the history. + while (!history.empty()) { + if (child->m_type != Type::OCTANT) { + // The neighbor is larger but still a neighbor! + return std::shared_ptr(child); + } + child = child->m_children[toggle_bit(history.back())]; + history.pop_back(); + } + + // We found a same-sized neighbor! + return std::shared_ptr(child); +} } // namespace inexor::vulkan_renderer::world diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d9b7cbeed..c897c8b1a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(INEXOR_UNIT_TEST_SOURCE_FILES unit_tests_main.cpp world/cube_collision.cpp + world/cube.cpp ) add_executable(inexor-vulkan-renderer-tests ${INEXOR_UNIT_TEST_SOURCE_FILES}) diff --git a/tests/world/cube.cpp b/tests/world/cube.cpp new file mode 100644 index 000000000..4341d0e1c --- /dev/null +++ b/tests/world/cube.cpp @@ -0,0 +1,30 @@ +#include + +#include + +namespace { +using namespace inexor::vulkan_renderer::world; + +TEST(Cube, neighbor) { + std::shared_ptr root = std::make_shared(2.0f, glm::vec3{0, -1, -1}); + root->set_type(Cube::Type::OCTANT); + + for (const auto &child : root->children()) { + child->set_type(Cube::Type::OCTANT); + + for (const auto &grandchild : child->children()) { + grandchild->set_type(Cube::Type::OCTANT); + } + } + + EXPECT_EQ(root->children()[1]->neighbor(Cube::NeighborAxis::Y, Cube::NeighborDirection::POSITIVE), + root->children()[3]); + EXPECT_NE(root->children()[1]->neighbor(Cube::NeighborAxis::Y, Cube::NeighborDirection::POSITIVE), + root->children()[0]); + EXPECT_EQ(root->children()[1]->children()[2]->neighbor(Cube::NeighborAxis::Y, Cube::NeighborDirection::POSITIVE), + root->children()[3]->children()[0]); + EXPECT_EQ(root->children()[1]->children()[2]->neighbor(Cube::NeighborAxis::Z, Cube::NeighborDirection::NEGATIVE), + root->children()[0]->children()[3]); +} + +} // namespace