// #define VALIDATE_AABB_TREE using UnityEngine; using System.Collections.Generic; using Pathfinding.Util; using Unity.Mathematics; using UnityEngine.Assertions; namespace Pathfinding.Collections { /// /// Axis Aligned Bounding Box Tree. /// /// Holds a bounding box tree with arbitrary data. /// /// The tree self-balances itself regularly when nodes are added. /// public class AABBTree { Node[] nodes = new Node[0]; int root = NoNode; readonly Stack freeNodes = new Stack(); int rebuildCounter = 64; const int NoNode = -1; struct Node { public Bounds bounds; public uint flags; const uint TagInsideBit = 1u << 30; const uint TagPartiallyInsideBit = 1u << 31; const uint AllocatedBit = 1u << 29; const uint ParentMask = ~(TagInsideBit | TagPartiallyInsideBit | AllocatedBit); public const int InvalidParent = (int)ParentMask; public bool wholeSubtreeTagged { get => (flags & TagInsideBit) != 0; set => flags = (flags & ~TagInsideBit) | (value ? TagInsideBit : 0); } public bool subtreePartiallyTagged { get => (flags & TagPartiallyInsideBit) != 0; set => flags = (flags & ~TagPartiallyInsideBit) | (value ? TagPartiallyInsideBit : 0); } public bool isAllocated { get => (flags & AllocatedBit) != 0; set => flags = (flags & ~AllocatedBit) | (value ? AllocatedBit : 0); } public bool isLeaf => left == NoNode; public int parent { get => (int)(flags & ParentMask); set => flags = (flags & ~ParentMask) | (uint)value; } public int left; public int right; public T value; } /// A key to a leaf node in the tree public readonly struct Key { internal readonly int value; public int node => value - 1; public bool isValid => value != 0; internal Key(int node) { this.value = node + 1; } } static float ExpansionRequired (Bounds b, Bounds b2) { var union = b; union.Encapsulate(b2); return union.size.x*union.size.y*union.size.z - b.size.x*b.size.y*b.size.z; } /// User data for a node in the tree public T this[Key key] => nodes[key.node].value; /// Bounding box of a given node public Bounds GetBounds (Key key) { if (!key.isValid) throw new System.ArgumentException("Key is not valid"); var node = nodes[key.node]; if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node"); if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node"); return node.bounds; } int AllocNode () { if (!freeNodes.TryPop(out int newNodeId)) { int prevLength = nodes.Length; Memory.Realloc(ref nodes, Mathf.Max(8, nodes.Length*2)); for (int i = nodes.Length - 1; i >= prevLength; i--) FreeNode(i); newNodeId = freeNodes.Pop(); #if VALIDATE_AABB_TREE Assert.IsFalse(nodes[newNodeId].isAllocated); #endif } return newNodeId; } void FreeNode (int node) { nodes[node].isAllocated = false; nodes[node].value = default; freeNodes.Push(node); } /// /// Rebuilds the whole tree. /// /// This can make it more balanced, and thus faster to query. /// public void Rebuild () { var leaves = new UnsafeSpan(Unity.Collections.Allocator.Temp, nodes.Length); int nLeaves = 0; for (int i = 0; i < nodes.Length; i++) { if (!nodes[i].isAllocated) continue; if (nodes[i].isLeaf) leaves[nLeaves++] = i; else FreeNode(i); } root = Rebuild(leaves.Slice(0, nLeaves), Node.InvalidParent); rebuildCounter = Mathf.Max(64, nLeaves / 3); Validate(root); } /// Removes all nodes from the tree public void Clear () { for (int i = 0; i < nodes.Length; i++) { if (nodes[i].isAllocated) FreeNode(i); } root = NoNode; rebuildCounter = 64; } struct AABBComparer : IComparer { public Node[] nodes; public int dim; public int Compare(int a, int b) => nodes[a].bounds.center[dim].CompareTo(nodes[b].bounds.center[dim]); } static int ArgMax (Vector3 v) { var m = Mathf.Max(v.x, Mathf.Max(v.y, v.z)); return m == v.x ? 0: (m == v.y ? 1 : 2); } int Rebuild (UnsafeSpan leaves, int parent) { if (leaves.Length == 0) return NoNode; if (leaves.Length == 1) { nodes[leaves[0]].parent = parent; return leaves[0]; } var bounds = nodes[leaves[0]].bounds; for (int i = 1; i < leaves.Length; i++) bounds.Encapsulate(nodes[leaves[i]].bounds); leaves.Sort(new AABBComparer { nodes = nodes, dim = ArgMax(bounds.extents) }); var nodeId = AllocNode(); nodes[nodeId] = new Node { bounds = bounds, left = Rebuild(leaves.Slice(0, leaves.Length/2), nodeId), right = Rebuild(leaves.Slice(leaves.Length/2), nodeId), parent = parent, isAllocated = true, }; return nodeId; } /// /// Moves a node to a new position. /// /// This will update the tree structure to account for the new bounding box. /// This is equivalent to removing the node and adding it again with the new bounds, but it preserves the key value. /// /// Key to the node to move /// New bounds of the node public void Move (Key key, Bounds bounds) { var value = nodes[key.node].value; Remove(key); var newKey = Add(bounds, value); // The first node added after a remove will have the same node index as the just removed node Assert.IsTrue(newKey.node == key.node); } [System.Diagnostics.Conditional("VALIDATE_AABB_TREE")] void Validate (int node) { if (node == NoNode) return; var n = nodes[node]; Assert.IsTrue(n.isAllocated); if (node == root) { Assert.AreEqual(Node.InvalidParent, n.parent); } else { Assert.AreNotEqual(Node.InvalidParent, n.parent); } if (n.isLeaf) { Assert.AreEqual(NoNode, n.right); } else { Assert.AreNotEqual(NoNode, n.right); Assert.AreNotEqual(n.left, n.right); Assert.AreEqual(node, nodes[n.left].parent); Assert.AreEqual(node, nodes[n.right].parent); Assert.IsTrue(math.all((float3)n.bounds.min <= (float3)nodes[n.left].bounds.min + 0.0001f)); Assert.IsTrue(math.all((float3)n.bounds.max >= (float3)nodes[n.left].bounds.max - 0.0001f)); Assert.IsTrue(math.all((float3)n.bounds.min <= (float3)nodes[n.right].bounds.min + 0.0001f)); Assert.IsTrue(math.all((float3)n.bounds.max >= (float3)nodes[n.right].bounds.max - 0.0001f)); Validate(n.left); Validate(n.right); } } public Bounds Remove (Key key) { if (!key.isValid) throw new System.ArgumentException("Key is not valid"); var node = nodes[key.node]; if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node"); if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node"); if (key.node == root) { root = NoNode; FreeNode(key.node); return node.bounds; } // Remove the parent from the tree and replace it with sibling var parentToRemoveId = node.parent; var parentToRemove = nodes[parentToRemoveId]; var siblingId = parentToRemove.left == key.node ? parentToRemove.right : parentToRemove.left; FreeNode(parentToRemoveId); FreeNode(key.node); nodes[siblingId].parent = parentToRemove.parent; if (parentToRemove.parent == Node.InvalidParent) { root = siblingId; } else { if (nodes[parentToRemove.parent].left == parentToRemoveId) { nodes[parentToRemove.parent].left = siblingId; } else { nodes[parentToRemove.parent].right = siblingId; } } // Rebuild bounding boxes var tmpNodeId = nodes[siblingId].parent; while (tmpNodeId != Node.InvalidParent) { ref var tmpNode = ref nodes[tmpNodeId]; var bounds = nodes[tmpNode.left].bounds; bounds.Encapsulate(nodes[tmpNode.right].bounds); tmpNode.bounds = bounds; tmpNode.subtreePartiallyTagged = nodes[tmpNode.left].subtreePartiallyTagged | nodes[tmpNode.right].subtreePartiallyTagged; tmpNodeId = tmpNode.parent; } Validate(root); return node.bounds; } public Key Add (Bounds bounds, T value) { var newNodeId = AllocNode(); nodes[newNodeId] = new Node { bounds = bounds, parent = Node.InvalidParent, left = NoNode, right = NoNode, value = value, isAllocated = true, }; if (root == NoNode) { root = newNodeId; Validate(root); return new Key(newNodeId); } int nodeId = root; while (true) { var node = nodes[nodeId]; // We can no longer guarantee that the whole subtree of this node is tagged, // as the new node is not tagged nodes[nodeId].wholeSubtreeTagged = false; if (node.isLeaf) { var newInnerId = AllocNode(); if (node.parent != Node.InvalidParent) { if (nodes[node.parent].left == nodeId) nodes[node.parent].left = newInnerId; else nodes[node.parent].right = newInnerId; } bounds.Encapsulate(node.bounds); nodes[newInnerId] = new Node { bounds = bounds, left = nodeId, right = newNodeId, parent = node.parent, isAllocated = true, }; nodes[newNodeId].parent = nodes[nodeId].parent = newInnerId; if (root == nodeId) root = newInnerId; if (rebuildCounter-- <= 0) Rebuild(); Validate(root); return new Key(newNodeId); } else { // Inner node nodes[nodeId].bounds.Encapsulate(bounds); float leftCost = ExpansionRequired(nodes[node.left].bounds, bounds); float rightCost = ExpansionRequired(nodes[node.right].bounds, bounds); nodeId = leftCost < rightCost ? node.left : node.right; } } } /// Queries the tree for all objects that touch the specified bounds. /// Bounding box to search within /// The results will be added to the buffer public void Query(Bounds bounds, List buffer) => QueryNode(root, bounds, buffer); void QueryNode (int node, Bounds bounds, List buffer) { if (node == NoNode || !bounds.Intersects(nodes[node].bounds)) return; if (nodes[node].isLeaf) { buffer.Add(nodes[node].value); } else { // Search children QueryNode(nodes[node].left, bounds, buffer); QueryNode(nodes[node].right, bounds, buffer); } } /// Queries the tree for all objects that have been previously tagged using the method. /// The results will be added to the buffer /// If true, all tags will be cleared after this call. If false, the tags will remain and can be queried again later. public void QueryTagged(List buffer, bool clearTags = false) => QueryTaggedNode(root, clearTags, buffer); void QueryTaggedNode (int node, bool clearTags, List buffer) { if (node == NoNode || !nodes[node].subtreePartiallyTagged) return; if (clearTags) { nodes[node].wholeSubtreeTagged = false; nodes[node].subtreePartiallyTagged = false; } if (nodes[node].isLeaf) { buffer.Add(nodes[node].value); } else { QueryTaggedNode(nodes[node].left, clearTags, buffer); QueryTaggedNode(nodes[node].right, clearTags, buffer); } } /// /// Tags a particular object. /// /// Any previously tagged objects stay tagged. /// You can retrieve the tagged objects using the method. /// /// Key to the object to tag public void Tag (Key key) { if (!key.isValid) throw new System.ArgumentException("Key is not valid"); if (key.node < 0 || key.node >= nodes.Length) throw new System.ArgumentException("Key does not point to a valid node"); ref var node = ref nodes[key.node]; if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node"); if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node"); node.wholeSubtreeTagged = true; int nodeId = key.node; while (nodeId != Node.InvalidParent) { nodes[nodeId].subtreePartiallyTagged = true; nodeId = nodes[nodeId].parent; } } /// /// Tags all objects that touch the specified bounds. /// /// Any previously tagged objects stay tagged. /// You can retrieve the tagged objects using the method. /// /// Bounding box to search within public void Tag(Bounds bounds) => TagNode(root, bounds); bool TagNode (int node, Bounds bounds) { if (node == NoNode || nodes[node].wholeSubtreeTagged) return true; // Nothing to do if (!bounds.Intersects(nodes[node].bounds)) return false; // TODO: Could make this less conservative by propagating info from the child nodes nodes[node].subtreePartiallyTagged = true; if (nodes[node].isLeaf) return nodes[node].wholeSubtreeTagged = true; else return nodes[node].wholeSubtreeTagged = TagNode(nodes[node].left, bounds) & TagNode(nodes[node].right, bounds); } } }