Fx2D
A C++20 2D rigid-body physics engine
Loading...
Searching...
No Matches
AABBTree.h
Go to the documentation of this file.
1#pragma once
2
3#include "Fx2D/Geometry.h"
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <utility>
8#include <vector>
9
10// Dynamic AABB tree (index-based pool, SAH-guided insertion).
11// Fat margin so small moves don't force constant reinsertion.
12static constexpr float AABB_TREE_MARGIN = 0.20f;
13
15 FxAABB fat_aabb; // fattened AABB (stored in tree)
16 FxAABB tight_aabb; // unfattened AABB (used for reinsertion check)
17 int32_t parent = -1;
18 int32_t left = -1;
19 int32_t right = -1;
20 int32_t entity_id = -1; // >= 0 for leaf nodes only
21 int32_t next_free = -1; // free-list chain (only valid when node is free)
22
23 bool is_leaf() const { return entity_id >= 0; }
24};
25
27 public:
28 FxAABBTree() = default;
29
30 // --- Insert a leaf for the given entity, returns its node index.
31 int32_t insert(int32_t entity_id, const FxAABB& tight) {
32 int32_t leaf = alloc_node();
33 node(leaf).tight_aabb = tight;
34 node(leaf).fat_aabb = fatten(tight);
35 node(leaf).entity_id = entity_id;
36 insert_leaf(leaf);
37 return leaf;
38 }
39
40 // --- Remove the leaf at node_idx and free the node.
41 void remove(int32_t node_idx) {
42 remove_leaf(node_idx);
43 free_node(node_idx);
44 }
45
46 // --- Update a leaf's AABB. Returns true if the fat box was rebuilt
47 // (i.e. entity moved far enough to escape its current fat box).
48 bool update(int32_t node_idx, const FxAABB& new_tight) {
49 FxAABBTreeNode& n = node(node_idx);
50 if (n.fat_aabb.contains(new_tight)) {
51 n.tight_aabb = new_tight;
52 return false; // still inside fat box, no reinsertion
53 }
54 remove_leaf(node_idx);
55 n.tight_aabb = new_tight;
56 n.fat_aabb = fatten(new_tight);
57 insert_leaf(node_idx);
58 return true;
59 }
60
61 // --- Collect every overlapping leaf-pair (entity_id_a < entity_id_b).
62 void query_pairs(std::vector<std::pair<int32_t, int32_t>>& out) const {
63 out.clear();
64 if (m_root < 0) return;
65 collect_pairs(m_root, m_root, out, /*same_node=*/true);
66 }
67
68 // --- Accessors
69 // Node handles are int32_t so -1 can mean "none". The cast to a vector index lives here
70 // once, rather than at each of the forty-odd subscripts Clang would warn about.
71 const FxAABBTreeNode& node(int32_t idx) const { return m_nodes[static_cast<std::size_t>(idx)]; }
72 FxAABBTreeNode& node(int32_t idx) { return m_nodes[static_cast<std::size_t>(idx)]; }
73 bool empty() const { return m_root < 0; }
74
75 private:
76 // Scratch for find_best_sibling's branch-and-bound descent. Mutable because the search is
77 // logically const; reused so the descent allocates nothing after the first insertion.
78 struct SiblingEntry {
79 int32_t idx;
80 float inherited_cost;
81 };
82 mutable std::vector<SiblingEntry> m_sibling_stack;
83
84 std::vector<FxAABBTreeNode> m_nodes;
85 int32_t m_root = -1;
86 int32_t m_free_head = -1;
87
88 // -------- node allocation pool --------
89 int32_t alloc_node() {
90 if (m_free_head >= 0) {
91 int32_t idx = m_free_head;
92 m_free_head = node(idx).next_free;
93 node(idx) = FxAABBTreeNode{};
94 return idx;
95 }
96 m_nodes.emplace_back();
97 return static_cast<int32_t>(m_nodes.size() - 1);
98 }
99
100 void free_node(int32_t idx) {
101 node(idx) = FxAABBTreeNode{};
102 node(idx).next_free = m_free_head;
103 m_free_head = idx;
104 }
105
106 // -------- AABB fattening --------
107 static FxAABB fatten(const FxAABB& b) {
108 float w = b.maxX - b.minX;
109 float h = b.maxY - b.minY;
110 // margin = 20% of each half-extent, with a minimum of 0.01 world units
111 float mx = std::max(w * AABB_TREE_MARGIN, 0.01f);
112 float my = std::max(h * AABB_TREE_MARGIN, 0.01f);
113 return {b.minX - mx, b.minY - my, b.maxX + mx, b.maxY + my};
114 }
115
116 // -------- SAH-guided insertion --------
117 void insert_leaf(int32_t leaf) {
118 if (m_root < 0) {
119 m_root = leaf;
120 node(leaf).parent = -1;
121 return;
122 }
123
124 int32_t best = find_best_sibling(leaf);
125 int32_t old_gp = node(best).parent;
126
127 int32_t np = alloc_node(); // new internal node
128 node(np).fat_aabb = FxAABB::combine(node(leaf).fat_aabb, node(best).fat_aabb);
129 node(np).entity_id = -1;
130 node(np).parent = old_gp;
131
132 if (old_gp >= 0) {
133 if (node(old_gp).left == best) node(old_gp).left = np;
134 else node(old_gp).right = np;
135 } else {
136 m_root = np;
137 }
138
139 node(np).left = best;
140 node(np).right = leaf;
141 node(best).parent = np;
142 node(leaf).parent = np;
143
144 refit_from(np);
145 }
146
147 // Best-sibling search using SAH lower-bound pruning
148 int32_t find_best_sibling(int32_t leaf) const {
149 const FxAABB& la = node(leaf).fat_aabb;
150 float best_cost = std::numeric_limits<float>::max();
151 int32_t best = m_root;
152
153 // A member, not a local: as a local it was one heap allocation and free per
154 // insertion, and a falling body is reinserted every substep it keeps moving.
155 m_sibling_stack.clear();
156 m_sibling_stack.push_back({m_root, 0.0f});
157
158 while (!m_sibling_stack.empty()) {
159 auto [idx, inh] = m_sibling_stack.back();
160 m_sibling_stack.pop_back();
161
162 FxAABB combined = FxAABB::combine(la, node(idx).fat_aabb);
163 float direct = combined.perimeter();
164 float total = direct + inh;
165
166 if (total < best_cost) {
167 best_cost = total;
168 best = idx;
169 }
170
171 if (!node(idx).is_leaf()) {
172 float child_inh = inh + direct - node(idx).fat_aabb.perimeter();
173 float lower_bound = la.perimeter() + child_inh;
174 if (lower_bound < best_cost) {
175 m_sibling_stack.push_back({node(idx).left, child_inh});
176 m_sibling_stack.push_back({node(idx).right, child_inh});
177 }
178 }
179 }
180 return best;
181 }
182
183 // -------- leaf removal --------
184 void remove_leaf(int32_t leaf) {
185 if (m_root == leaf) {
186 m_root = -1;
187 return;
188 }
189
190 int32_t parent = node(leaf).parent;
191 int32_t grandpa = node(parent).parent;
192 int32_t sibling = (node(parent).left == leaf) ? node(parent).right : node(parent).left;
193
194 if (grandpa >= 0) {
195 if (node(grandpa).left == parent) node(grandpa).left = sibling;
196 else node(grandpa).right = sibling;
197 node(sibling).parent = grandpa;
198 free_node(parent);
199 refit_from(grandpa);
200 } else {
201 // parent was the root
202 m_root = sibling;
203 node(sibling).parent = -1;
204 free_node(parent);
205 }
206 node(leaf).parent = -1;
207 }
208
209 // Refit fat_aabb upward to the root after a structural change
210 void refit_from(int32_t idx) {
211 for (; idx >= 0; idx = node(idx).parent) {
212 int32_t l = node(idx).left;
213 int32_t r = node(idx).right;
214 if (l >= 0 && r >= 0)
215 node(idx).fat_aabb = FxAABB::combine(node(l).fat_aabb, node(r).fat_aabb);
216 else if (l >= 0) node(idx).fat_aabb = node(l).fat_aabb;
217 else if (r >= 0) node(idx).fat_aabb = node(r).fat_aabb;
218 }
219 }
220
221 // -------- overlapping pair collection (descent-based) --------
222 void collect_pairs(int32_t a, int32_t b, std::vector<std::pair<int32_t, int32_t>>& out,
223 bool same_node) const {
224 if (a < 0 || b < 0) return;
225 const FxAABBTreeNode& na = node(a);
226 const FxAABBTreeNode& nb = node(b);
227
228 if (same_node) {
229 if (na.is_leaf()) return;
230 collect_pairs(na.left, na.left, out, true);
231 collect_pairs(na.left, na.right, out, false);
232 collect_pairs(na.right, na.right, out, true);
233 } else {
234 if (!na.fat_aabb.overlaps(nb.fat_aabb)) return;
235 if (na.is_leaf() && nb.is_leaf()) {
236 int32_t x = na.entity_id, y = nb.entity_id;
237 if (x != y) {
238 if (x > y) std::swap(x, y);
239 out.emplace_back(x, y);
240 }
241 return;
242 }
243 // Descend into the larger node
244 if (nb.is_leaf() ||
245 (!na.is_leaf() && na.fat_aabb.perimeter() >= nb.fat_aabb.perimeter())) {
246 collect_pairs(na.left, b, out, false);
247 collect_pairs(na.right, b, out, false);
248 } else {
249 collect_pairs(a, nb.left, out, false);
250 collect_pairs(a, nb.right, out, false);
251 }
252 }
253 }
254};
Dynamic broad-phase tree used to find candidate pairs.
Definition AABBTree.h:26
FxAABBTreeNode & node(int32_t idx)
Definition AABBTree.h:72
FxAABBTree()=default
const FxAABBTreeNode & node(int32_t idx) const
Definition AABBTree.h:71
bool empty() const
Definition AABBTree.h:73
void query_pairs(std::vector< std::pair< int32_t, int32_t > > &out) const
Definition AABBTree.h:62
bool update(int32_t node_idx, const FxAABB &new_tight)
Definition AABBTree.h:48
int32_t insert(int32_t entity_id, const FxAABB &tight)
Definition AABBTree.h:31
void remove(int32_t node_idx)
Definition AABBTree.h:41
int32_t next_free
Definition AABBTree.h:21
bool is_leaf() const
Definition AABBTree.h:23
FxAABB fat_aabb
Definition AABBTree.h:15
int32_t entity_id
Definition AABBTree.h:20
int32_t parent
Definition AABBTree.h:17
FxAABB tight_aabb
Definition AABBTree.h:16
int32_t right
Definition AABBTree.h:19
int32_t left
Definition AABBTree.h:18
Axis-aligned bounding box used by broad-phase operations.
Definition Geometry.h:20
float minY
Definition Geometry.h:21
bool contains(const FxAABB &inner) const
Definition Geometry.h:36
float maxY
Definition Geometry.h:21
float perimeter() const
Definition Geometry.h:32
float maxX
Definition Geometry.h:21
float minX
Definition Geometry.h:21
bool overlaps(const FxAABB &o) const
Definition Geometry.h:33
static FxAABB combine(const FxAABB &a, const FxAABB &b)
Definition Geometry.h:25