Skip to content

Commit

Permalink
Added a QuadTree class.
Browse files Browse the repository at this point in the history
First pass at a quad tree implementation for use in terrain LOD.
Added basic unit tests and requires std::formatters.
  • Loading branch information
MStachowicz committed Dec 1, 2024
1 parent 4cb901b commit c313fd0
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ source/Test/Tests/ResourceManagerTester.hpp
source/Test/Tests/ResourceManagerTester.cpp
source/Test/Tests/GeometryTester.hpp
source/Test/Tests/GeometryTester.cpp
source/Test/Tests/QuadTreeTester.hpp
source/Test/Tests/QuadTreeTester.cpp
)
target_include_directories(Test
PRIVATE source/Test/Tests
Expand Down Expand Up @@ -210,6 +212,7 @@ source/Geometry/Plane.hpp
source/Geometry/Point.hpp
source/Geometry/Quad.hpp
source/Geometry/Quad.cpp
source/Geometry/QuadTree.hpp
source/Geometry/Ray.hpp
source/Geometry/Sphere.hpp
source/Geometry/Sphere.cpp
Expand Down
259 changes: 259 additions & 0 deletions source/Geometry/QuadTree.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
#include "AABB.hpp"

#include "Utility/Logger.hpp"

#include <algorithm>
#include <array>
#include <optional>
#include <stdexcept>
#include <vector>

namespace Geometry
{
// Quad tree data structure for 2D space partitioning.
// Each node has a 2D axis-aligned bounding box (AABB) and can have 0 or 4 child nodes.
// The tree is stored in a vector and uses lazy deletion on node removal. Removed nodes don't free memory
class QuadTree
{
bool is_free(size_t index) const { return std::find(free_indices.begin(), free_indices.end(), index) != free_indices.end(); }
// Add a node to the tree. If there are no free indices, a new node is added to the end of the nodes vector.
// Otherwise, the node at a free index is replaced with the new node.
// add_node invalidates any iterators or pointers to Node objects.
// @returns The index of the new node.
size_t add_node(const AABB2D& bounds, size_t depth)
{
if (free_indices.empty())
{
nodes.push_back(Node{bounds, depth});
return nodes.size() - 1;
}
else
{
nodes[free_indices.back()] = Node{bounds, depth};
size_t index = free_indices.back();
free_indices.pop_back();
return index;
}
}
public:
// A node in the quad tree.
// Node references are invalidated when the tree is modified.
struct Node
{
Node(const AABB2D& bounds, size_t depth) : bounds{bounds}, children_indices{std::nullopt}, depth{depth} {}

bool leaf() const { return !children_indices.has_value(); }
size_t top_left() const { return (*children_indices)[0]; }
size_t top_right() const { return (*children_indices)[1]; }
size_t bottom_left() const { return (*children_indices)[2]; }
size_t bottom_right() const { return (*children_indices)[3]; }

AABB2D bounds;
std::optional<std::array<size_t, 4>> children_indices; // top-left, top-right, bottom-right, bottom-left
size_t depth;
};

QuadTree() : nodes{}, free_indices{} {}
// @returns The number of nodes in the tree.
size_t size() const { return nodes.size() - free_indices.size(); }
// @returns True if the tree has no nodes.
bool empty() const { return size() == 0; }
// @returns The maximum depth of the tree. Root node has depth 0.
size_t depth() const
{
size_t max_depth = 0;
for (auto it = begin(); it != end(); ++it)
max_depth = std::max(max_depth, it->depth);
return max_depth;
}
// Reserve space for the specified number of nodes.
void reserve(size_t size) { nodes.reserve(size); }


size_t add_root_node(const AABB2D& bounds)
{
if (!empty()) throw std::runtime_error("Root node already exists.");
return add_node(bounds, 0);
}
Node& root_node()
{
if (empty()) throw std::runtime_error("No root node exists.");
return nodes[0];
}
const Node& root_node() const
{
if (empty()) throw std::runtime_error("No root node exists.");
return nodes[0];
}
// Divide this node into 4 children nodes. May cause a reallocation invalidating any iterators or pointers to the nodes.
//@returns The index of the first child node (top-left).
size_t subdivide(Node& node)
{
if (node.children_indices.has_value())
throw std::runtime_error("Node already subdivided");

const auto& bounds = node.bounds;
const auto center = bounds.center();

// Define child bounds in clockwise order: top-left, top-right, bottom-right, bottom-left
const std::array<AABB2D, 4> child_bounds = {
AABB2D{glm::vec2{bounds.min.x, center.y}, glm::vec2{center.x, bounds.max.y}},
AABB2D{center, bounds.max},
AABB2D{bounds.min, center},
AABB2D{glm::vec2{center.x, bounds.min.y}, glm::vec2{bounds.max.x, center.y}}
};

size_t index = &node - &nodes[0]; // Get the index of the node before add_node may invalidate the reference
size_t new_depth = node.depth + 1; // Grab node.depth before it is invalidated by add_node

std::array<size_t, 4> indices;
for (size_t i = 0; i < 4; ++i)
indices[i] = add_node(child_bounds[i], new_depth);

nodes[index].children_indices = indices;
return indices[0]; // Return top-left child index
}
// Merge the children of this node into the node. Does not invalidate any iterators or pointers to the nodes.
void merge(Node& node)
{
if (node.leaf())
throw std::runtime_error("Cannot merge a leaf node.");

// DOESNT DELETE THE NODE, only marks it as free.
for (size_t i = 0; i < 4; ++i)
free_indices.push_back((*node.children_indices)[i]);

node.children_indices.reset();
}
size_t node_index(const Node& node) const { return &node - &nodes[0]; };

template <typename Func>
requires std::invocable<Func, Node&>
void for_each_child(Node& node, Func&& func)
{
if (node.leaf())
return;

for (size_t i = 0; i < 4; ++i)
func(nodes[(*node.children_indices)[i]]);
}

// Depth-first traversal starting from the specified node.
template <typename Func>
requires std::invocable<Func, Node&>
void depth_first_traversal(const Node& start_node, Func&& func)
{
std::vector<size_t> stack;
stack.push_back(&start_node - &nodes[0]);

while (!stack.empty())
{
size_t index = stack.back();
stack.pop_back();

if (is_free(index))
continue;

Node& node = nodes[index];

if constexpr (std::is_same_v<std::invoke_result_t<Func, Node&>, bool>)
{
if (func(node))
return;
}
else
func(node);

if (!node.leaf())
for (size_t i = 0; i < 4; ++i)
stack.push_back((*node.children_indices)[i]);
}
}
// Depth-first traversal starting from the root node.
template <typename Func>
void depth_first_traversal(Func&& func) { depth_first_traversal(root_node(), func); }

// Breadth-first traversal starting from the root node.
template <typename Func>
void breadth_first_traversal(Func&& func) { breadth_first_traversal(root_node(), func); }
// Breadth-first traversal starting from the specified node.
template <typename Func>
requires std::invocable<Func, Node&>
void breadth_first_traversal(Node& start_node, Func&& func)
{
std::vector<size_t> queue;
queue.push_back(&start_node - &nodes[0]);

while (!queue.empty())
{
size_t index = queue.front();
queue.erase(queue.begin());

if (is_free(index))
continue;

Node& node = nodes[index];
func(node);

if (!node.leaf())
for (size_t i = 0; i < 4; ++i)
queue.push_back((*node.children_indices)[i]);
}
}

struct QuadTreeIterator
{
QuadTree& quad_tree;
size_t index;

QuadTreeIterator(QuadTree& quad_tree, size_t index) : quad_tree{quad_tree}, index{index} {}
QuadTreeIterator& operator++()
{
do { ++index; }
while (index < quad_tree.nodes.size() && quad_tree.is_free(index));
return *this;
}
Node& operator*() { return quad_tree[index]; }
Node* operator->() { return &quad_tree[index]; }
bool operator!=(const QuadTreeIterator& other) const { return index != other.index; }
};
struct ConstQuadTreeIterator
{
const QuadTree& quad_tree;
size_t index;

ConstQuadTreeIterator(const QuadTree& quad_tree, size_t index) : quad_tree{quad_tree}, index{index} {}
ConstQuadTreeIterator& operator++()
{
do { ++index; }
while (index < quad_tree.nodes.size() && quad_tree.is_free(index));
return *this;
}
const Node& operator*() { return quad_tree[index]; }
const Node* operator->() { return &quad_tree[index]; }
bool operator!=(const ConstQuadTreeIterator& other) const { return index != other.index; }
};
QuadTreeIterator begin() { return QuadTreeIterator(*this, 0); }
QuadTreeIterator end() { return QuadTreeIterator(*this, nodes.size()); }
ConstQuadTreeIterator begin() const { return ConstQuadTreeIterator(*this, 0); }
ConstQuadTreeIterator end() const { return ConstQuadTreeIterator(*this, nodes.size()); }
ConstQuadTreeIterator cbegin() const noexcept { return begin(); }
ConstQuadTreeIterator cend() const noexcept { return end(); }

Node& operator[](size_t index)
{
ASSERT(!is_free(index), "Cannot access a freed index.");
ASSERT(index < nodes.size(), "Index out of bounds.");
return nodes[index];
}
const Node& operator[](size_t index) const
{
ASSERT(!is_free(index), "Cannot access a freed index.");
ASSERT(index < nodes.size(), "Index out of bounds.");
return nodes[index];
}
private:
std::vector<Node> nodes; // Stores valid and free nodes.
std::vector<size_t> free_indices; // Indices into nodes vector that are free.
};
}// namespace Geometry
2 changes: 2 additions & 0 deletions source/Test/TestMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Test/Tests/GeometryTester.hpp"
#include "Test/Tests/ResourceManagerTester.hpp"
#include "Test/Tests/GraphicsTester.hpp"
#include "Test/Tests/QuadTreeTester.hpp"

#include <cstring>
#include "Utility/Stopwatch.hpp"
Expand Down Expand Up @@ -40,6 +41,7 @@ int main(int argc, char* argv[])
test_managers.emplace_back(std::make_unique<Test::ECSTester>());
test_managers.emplace_back(std::make_unique<Test::GeometryTester>());
test_managers.emplace_back(std::make_unique<Test::ResourceManagerTester>());
test_managers.emplace_back(std::make_unique<Test::QuadTreeTester>());
if (!skip_graphics_test)
test_managers.emplace_back(std::make_unique<Test::GraphicsTester>());

Expand Down
19 changes: 19 additions & 0 deletions source/Test/TestManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "glm/gtc/quaternion.hpp"
#include "glm/gtc/epsilon.hpp"
#include "Geometry/Triangle.hpp"
#include "Geometry/AABB.hpp"

#include <string>
#include <vector>
Expand All @@ -24,6 +25,12 @@ namespace std
}
};
template<>
struct formatter<glm::vec2>
{
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const glm::vec2& v, format_context& ctx) const { return format_to(ctx.out(), "({}, {})", v.x, v.y); }
};
template<>
struct formatter<glm::vec3>
{
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
Expand All @@ -41,6 +48,18 @@ namespace std
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const Geometry::Triangle& v, format_context& ctx) const { return format_to(ctx.out(), "({}, {}, {})", v.m_point_1, v.m_point_2, v.m_point_3); }
};
template<>
struct formatter<Geometry::AABB>
{
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const Geometry::AABB& v, format_context& ctx) const { return format_to(ctx.out(), "({}, {})", v.m_min, v.m_max); }
};
template<>
struct formatter<Geometry::AABB2D>
{
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const Geometry::AABB2D& v, format_context& ctx) const { return format_to(ctx.out(), "({}, {})", v.min, v.max); }
};
}

namespace Test
Expand Down
55 changes: 55 additions & 0 deletions source/Test/Tests/QuadTreeTester.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "QuadTreeTester.hpp"
#include "Geometry/QuadTree.hpp"

namespace Test
{
void QuadTreeTester::run_unit_tests()
{
SCOPE_SECTION("QuadTree")
{
constexpr float min = 0.f;
constexpr float max = 100.f;
constexpr float mid = (min + max) / 2.f;

{
Geometry::QuadTree quad_tree;
{SCOPE_SECTION("Empty")
CHECK_TRUE(quad_tree.empty(), "Empty");
CHECK_EQUAL(quad_tree.depth(), 0, "Depth");
CHECK_EQUAL(quad_tree.size(), 0, "Size");
}
{SCOPE_SECTION("Add root node")
quad_tree.add_root_node(Geometry::AABB2D{glm::vec2{min}, glm::vec2{max}});
CHECK_TRUE(!quad_tree.empty(), "Not empty");
CHECK_EQUAL(quad_tree.depth(), 0, "Depth");
CHECK_EQUAL(quad_tree.size(), 1, "Size");
}
{SCOPE_SECTION("Subdivide root")
quad_tree.subdivide(quad_tree.root_node());
CHECK_TRUE(!quad_tree.empty(), "Not empty");
CHECK_EQUAL(quad_tree.depth(), 1, "Depth after subdivision");
CHECK_EQUAL(quad_tree.size(), 5, "Size after subdivision");

std::array<Geometry::AABB2D, 4> expected_bounds = { // Clockwise from top-left
Geometry::AABB2D{glm::vec2{min, mid}, glm::vec2{mid, max}},
Geometry::AABB2D{glm::vec2{mid, mid}, glm::vec2{max, max}},
Geometry::AABB2D{glm::vec2{min, min}, glm::vec2{mid, mid}},
Geometry::AABB2D{glm::vec2{mid, min}, glm::vec2{max, mid}}
};
size_t index = 0;
quad_tree.for_each_child(quad_tree.root_node(), [&](Geometry::QuadTree::Node& node)
{
CHECK_EQUAL(node.bounds, expected_bounds[index], "Root child bounds");
++index;
});
}
{SCOPE_SECTION("Merge root")
quad_tree.merge(quad_tree.root_node());
CHECK_TRUE(!quad_tree.empty(), "Not empty");
CHECK_EQUAL(quad_tree.depth(), 0, "Depth after merge");
CHECK_EQUAL(quad_tree.size(), 1, "Size after merge");
}
}
}
}
} // namespace Test
Loading

0 comments on commit c313fd0

Please sign in to comment.