diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs new file mode 100644 index 0000000..5794204 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs @@ -0,0 +1,381 @@ +// #define VALIDATE_AABB_TREE +using UnityEngine; +using System.Collections.Generic; +using Pathfinding.Util; +using Unity.Mathematics; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Navmesh { + /// <summary> + /// Axis Aligned Bounding Box Tree. + /// + /// Holds a bounding box tree with arbitrary data. + /// + /// The tree self-balances itself regularly when nodes are added. + /// </summary> + public class AABBTree<T> { + Node[] nodes = new Node[0]; + int root = NoNode; + readonly Stack<int> freeNodes = new Stack<int>(); + 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; + } + + /// <summary>A key to a leaf node in the tree</summary> + 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; + } + + /// <summary>User data for a node in the tree</summary> + public T this[Key key] => nodes[key.node].value; + + /// <summary>Bounding box of a given node</summary> + 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); + } + + /// <summary> + /// Rebuilds the whole tree. + /// + /// This can make it more balanced, and thus faster to query. + /// </summary> + public void Rebuild () { + var leaves = new UnsafeSpan<int>(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); + } + + /// <summary>Removes all nodes from the tree</summary> + public void Clear () { + for (int i = 0; i < nodes.Length; i++) { + if (nodes[i].isAllocated) FreeNode(i); + } + root = NoNode; + rebuildCounter = 64; + } + + struct AABBComparer : IComparer<int> { + 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<int> 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; + } + + /// <summary> + /// 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. + /// </summary> + /// <param name="key">Key to the node to move</param> + /// <param name="bounds">New bounds of the node</param> + 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; + } + } + } + + /// <summary>Queries the tree for all objects that touch the specified bounds.</summary> + /// <param name="bounds">Bounding box to search within</param> + /// <param name="buffer">The results will be added to the buffer</param> + public void Query(Bounds bounds, List<T> buffer) => QueryNode(root, bounds, buffer); + + void QueryNode (int node, Bounds bounds, List<T> 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); + } + } + + /// <summary>Queries the tree for all objects that have been previously tagged using the <see cref="Tag"/> method.</summary> + /// <param name="buffer">The results will be added to the buffer</param> + /// <param name="clearTags">If true, all tags will be cleared after this call. If false, the tags will remain and can be queried again later.</param> + public void QueryTagged(List<T> buffer, bool clearTags = false) => QueryTaggedNode(root, clearTags, buffer); + + void QueryTaggedNode (int node, bool clearTags, List<T> 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); + } + } + + /// <summary> + /// Tags a particular object. + /// + /// Any previously tagged objects stay tagged. + /// You can retrieve the tagged objects using the <see cref="QueryTagged"/> method. + /// </summary> + /// <param name="key">Key to the object to tag</param> + 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; + } + } + + /// <summary> + /// Tags all objects that touch the specified bounds. + /// + /// Any previously tagged objects stay tagged. + /// You can retrieve the tagged objects using the <see cref="QueryTagged"/> method. + /// </summary> + /// <param name="bounds">Bounding box to search within</param> + 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); + } + } +} |