Ensure that the Octree is balanced.

This means that two adjacent nodes can only have at most one level of refinement difference.

Note: This is a small breaking change for mjWarp since the order of the children was flipped.
PiperOrigin-RevId: 806299445
Change-Id: Ia326a31c7a8162ae02d70c868d87779198e805be
This commit is contained in:
Alessio Quaglino
2025-09-12 08:33:25 -07:00
committed by Copybara-Service
parent 2287b9f815
commit 4fc69fa64d
4 changed files with 245 additions and 16 deletions
+146 -10
View File
@@ -692,16 +692,18 @@ static bool boxTriangle(const Triangle& v, const double aamm[6]) {
void mjCOctree::TaskToNode(const OctreeTask& task, OctNode& node,
std::unordered_map<Point, int>& vert_map) {
node.level = task.lev;
node.parent_index = task.parent_index;
node.child_slot = task.child_slot;
if (task.parent_index != -1) {
node_[task.parent_index].child[task.child_slot] = task.node_index;
const auto parent_aamm = node_[task.parent_index].aamm;
node.aamm[0] = task.child_slot & 1 ? parent_aamm[0] : (parent_aamm[3] + parent_aamm[0]) / 2;
node.aamm[1] = task.child_slot & 2 ? parent_aamm[1] : (parent_aamm[4] + parent_aamm[1]) / 2;
node.aamm[2] = task.child_slot & 4 ? parent_aamm[2] : (parent_aamm[5] + parent_aamm[2]) / 2;
node.aamm[3] = task.child_slot & 1 ? (parent_aamm[0] + parent_aamm[3]) / 2 : parent_aamm[3];
node.aamm[4] = task.child_slot & 2 ? (parent_aamm[1] + parent_aamm[4]) / 2 : parent_aamm[4];
node.aamm[5] = task.child_slot & 4 ? (parent_aamm[2] + parent_aamm[5]) / 2 : parent_aamm[5];
node.aamm[0] = task.child_slot & 1 ? (parent_aamm[3] + parent_aamm[0]) / 2 : parent_aamm[0];
node.aamm[1] = task.child_slot & 2 ? (parent_aamm[4] + parent_aamm[1]) / 2 : parent_aamm[1];
node.aamm[2] = task.child_slot & 4 ? (parent_aamm[5] + parent_aamm[2]) / 2 : parent_aamm[2];
node.aamm[3] = task.child_slot & 1 ? parent_aamm[3] : (parent_aamm[0] + parent_aamm[3]) / 2;
node.aamm[4] = task.child_slot & 2 ? parent_aamm[4] : (parent_aamm[1] + parent_aamm[4]) / 2;
node.aamm[5] = task.child_slot & 4 ? parent_aamm[5] : (parent_aamm[2] + parent_aamm[5]) / 2;
}
for (int i = 0; i < 8; i++) {
@@ -721,8 +723,8 @@ void mjCOctree::TaskToNode(const OctreeTask& task, OctNode& node,
}
void mjCOctree::Subdivide(std::deque<OctreeTask>& queue, const std::vector<Triangle*>& colliding,
const OctreeTask& task, std::unordered_map<Point, int>& vert_map) {
void mjCOctree::Subdivide(const OctreeTask& task, std::unordered_map<Point, int>& vert_map,
std::deque<OctreeTask>* queue, const std::vector<Triangle*>& colliding) {
for (int i = 0; i < 8; i++) {
OctreeTask new_task;
new_task.elements = colliding;
@@ -733,7 +735,138 @@ void mjCOctree::Subdivide(std::deque<OctreeTask>& queue, const std::vector<Trian
node_.push_back(OctNode());
TaskToNode(new_task, node_.back(), vert_map);
queue.push_back(std::move(new_task));
if (queue) {
queue->push_back(std::move(new_task));
}
}
}
// recursively finds the adjacent ancestor neighbor region
int mjCOctree::FindCoarseNeighbor(int node_idx, int dir) {
if (node_idx == -1) {
return -1;
}
int parent_idx = node_[node_idx].parent_index;
// if we are at the root, we have no parent and thus no siblings or external neighbors
if (parent_idx == -1) {
return -1;
}
int child_slot = node_[node_idx].child_slot;
int dim = dir / 2;
int side = dir % 2;
int bit = 1 << dim;
if (side != ((child_slot & bit) != 0)) {
// internal neighbor case: This is the successful termination of the climb
// return the adjacent sibling node
return node_[parent_idx].child[child_slot ^ bit];
} else {
// external neighbor case: Recurse up the tree
// ask our parent to find its neighbor in the same direction
return FindCoarseNeighbor(parent_idx, dir);
}
}
int mjCOctree::FindNeighbor(int node_idx, int dir) {
if (node_idx == -1) {
return -1;
}
// call the helper to find the adjacent to the coarse neighbor.
// this might be an internal node (e.g., our parent's sibling)
int result = FindCoarseNeighbor(node_idx, dir);
if (result == -1) {
// no neighbor found (either at tree boundary or some other error)
return -1;
}
// leaf descent
double node_center[3] = {
(node_[node_idx].aamm[0] + node_[node_idx].aamm[3]) / 2,
(node_[node_idx].aamm[1] + node_[node_idx].aamm[4]) / 2,
(node_[node_idx].aamm[2] + node_[node_idx].aamm[5]) / 2,
};
while (node_[result].child[0] != -1) {
double result_center[3] = {
(node_[result].aamm[0] + node_[result].aamm[3]) / 2,
(node_[result].aamm[1] + node_[result].aamm[4]) / 2,
(node_[result].aamm[2] + node_[result].aamm[5]) / 2,
};
// find relative octant of our node w.r.t. the neighbor's center
int next_child_slot = 0;
if (node_center[0] > result_center[0]) next_child_slot |= 1;
if (node_center[1] > result_center[1]) next_child_slot |= 2;
if (node_center[2] > result_center[2]) next_child_slot |= 4;
int dim = dir / 2;
int side = dir % 2;
int bit = 1 << dim;
// we need the child on the opposite side (adjacent to this node)
int op_side = (side != 1);
next_child_slot = (next_child_slot & ~bit) | (op_side * bit);
result = node_[result].child[next_child_slot];
}
return result;
}
// refine the octree by subdividing nodes that are too coarse such that the
// maximum level difference between adjacent nodes is at most 1.
void mjCOctree::BalanceOctree(std::unordered_map<Point, int>& vert_map) {
bool changed = true;
while (changed) {
changed = false;
std::vector<int> leaves;
for (int i = 0; i < nnode_; ++i) {
if (node_[i].child[0] == -1) {
leaves.push_back(i);
}
}
// find the nodes that are too coarse, only leaves need to be checked
std::vector<int> leaves_to_subdivide;
for (int leaf_idx : leaves) {
if (node_[leaf_idx].child[0] != -1) {
continue;
}
for (int dir = 0; dir < 6; ++dir) {
int neighbor_idx = FindNeighbor(leaf_idx, dir);
if (neighbor_idx == -1) {
continue;
}
int neighbor_level = node_[neighbor_idx].level;
if (neighbor_level > node_[leaf_idx].level + 1) {
leaves_to_subdivide.push_back(leaf_idx);
}
if (node_[leaf_idx].level > neighbor_level + 1) {
leaves_to_subdivide.push_back(neighbor_idx);
}
}
}
// subdivide the nodes that are too coarse
if (!leaves_to_subdivide.empty()) {
changed = true;
for (int node_idx : leaves_to_subdivide) {
if (node_[node_idx].child[0] == -1) { // check if not already subdivided
OctreeTask task;
task.node_index = node_idx;
task.lev = node_[node_idx].level;
Subdivide(task, vert_map);
}
}
}
}
}
@@ -773,8 +906,11 @@ void mjCOctree::MakeOctree(const std::vector<Triangle*>& elements, const double
}
// subdivide the node
Subdivide(queue, colliding, task, vert_map);
Subdivide(task, vert_map, &queue, colliding);
}
// store the neighbors of each node
BalanceOctree(vert_map);
}
//------------------------- class mjCDef implementation --------------------------------------------