//#define ASTARDEBUG //"BBTree Debug" If enables, some queries to the tree will show debug lines. Turn off multithreading when using this since DrawLine calls cannot be called from a different thread using System; using System.Collections.Generic; using UnityEngine; using Unity.Mathematics; using Unity.Burst; using Unity.Profiling; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Pathfinding.Drawing; namespace Pathfinding.Collections { using Pathfinding.Util; /// /// Axis Aligned Bounding Box Tree. /// Holds a bounding box tree of triangles. /// [BurstCompile] public struct BBTree : IDisposable { /// Holds all tree nodes UnsafeList tree; UnsafeList nodePermutation; const int MaximumLeafSize = 4; public IntRect Size => tree.Length == 0 ? default : tree[0].rect; // We need a stack while searching the tree. // We use a stack allocated array for this to avoid allocations. // A tile can at most contain NavmeshBase.VertexIndexMask triangles. // This works out to about a million. A perfectly balanced tree can fit this in log2(1000000/4) = 18 levels. // but we add a few more levels just to be safe, in case the tree is not perfectly balanced. const int MAX_TREE_HEIGHT = 26; public void Dispose () { nodePermutation.Dispose(); tree.Dispose(); } /// Build a BBTree from a list of triangles. /// The triangles. Each triplet of 3 indices represents a node. The triangles are assumed to be in clockwise order. /// The vertices of the triangles. public BBTree(UnsafeSpan triangles, UnsafeSpan vertices) { if (triangles.Length % 3 != 0) throw new ArgumentException("triangles must be a multiple of 3 in length"); Build(ref triangles, ref vertices, out this); } [BurstCompile] static void Build (ref UnsafeSpan triangles, ref UnsafeSpan vertices, out BBTree bbTree) { var nodeCount = triangles.Length/3; // We will use approximately 2N tree nodes var tree = new UnsafeList((int)(nodeCount * 2.1f), Allocator.Persistent, NativeArrayOptions.UninitializedMemory); // We will use approximately N node references var nodes = new UnsafeList((int)(nodeCount * 1.1f), Allocator.Persistent, NativeArrayOptions.UninitializedMemory); // This will store the order of the nodes while the tree is being built // It turns out that it is a lot faster to do this than to actually modify // the nodes and nodeBounds arrays (presumably since that involves shuffling // around 20 bytes of memory (sizeof(pointer) + sizeof(IntRect)) per node // instead of 4 bytes (sizeof(int)). // It also means we don't have to make a copy of the nodes array since // we do not modify it var permutation = new NativeArray(nodeCount, Allocator.Temp); for (int i = 0; i < nodeCount; i++) { permutation[i] = i; } // Precalculate the bounds of the nodes in XZ space. // It turns out that calculating the bounds is a bottleneck and precalculating // the bounds makes it around 3 times faster to build a tree var nodeBounds = new NativeArray(nodeCount, Allocator.Temp); for (int i = 0; i < nodeCount; i++) { var v0 = ((int3)vertices[triangles[i*3+0]]).xz; var v1 = ((int3)vertices[triangles[i*3+1]]).xz; var v2 = ((int3)vertices[triangles[i*3+2]]).xz; var mn = math.min(v0, math.min(v1, v2)); var mx = math.max(v0, math.max(v1, v2)); nodeBounds[i] = new IntRect(mn.x, mn.y, mx.x, mx.y); } if (nodeCount > 0) BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, 0, nodeCount, false, 0); nodeBounds.Dispose(); permutation.Dispose(); bbTree = new BBTree { tree = tree, nodePermutation = nodes, }; } static int SplitByX (NativeArray nodesBounds, NativeArray permutation, int from, int to, int divider) { int mx = to; for (int i = from; i < mx; i++) { var cr = nodesBounds[permutation[i]]; var cx = (cr.xmin + cr.xmax)/2; if (cx > divider) { mx--; // Swap items i and mx var tmp = permutation[mx]; permutation[mx] = permutation[i]; permutation[i] = tmp; i--; } } return mx; } static int SplitByZ (NativeArray nodesBounds, NativeArray permutation, int from, int to, int divider) { int mx = to; for (int i = from; i < mx; i++) { var cr = nodesBounds[permutation[i]]; var cx = (cr.ymin + cr.ymax)/2; if (cx > divider) { mx--; // Swap items i and mx var tmp = permutation[mx]; permutation[mx] = permutation[i]; permutation[i] = tmp; i--; } } return mx; } static int BuildSubtree (NativeArray permutation, NativeArray nodeBounds, ref UnsafeList nodes, ref UnsafeList tree, int from, int to, bool odd, int depth) { var rect = NodeBounds(permutation, nodeBounds, from, to); int boxId = tree.Length; tree.Add(new BBTreeBox(rect)); if (to - from <= MaximumLeafSize) { if (depth > MAX_TREE_HEIGHT) { Debug.LogWarning($"Maximum tree height of {MAX_TREE_HEIGHT} exceeded (got depth of {depth}). Querying this tree may fail. Is the tree very unbalanced?"); } var box = tree[boxId]; var nodeOffset = box.nodeOffset = nodes.Length; tree[boxId] = box; nodes.Length += MaximumLeafSize; // Assign all nodes to the array. Note that we also need clear unused slots as the array from the pool may contain any information for (int i = 0; i < MaximumLeafSize; i++) { nodes[nodeOffset + i] = i < to - from ? permutation[from + i] : -1; } return boxId; } else { int splitIndex; if (odd) { // X int divider = (rect.xmin + rect.xmax)/2; splitIndex = SplitByX(nodeBounds, permutation, from, to, divider); } else { // Y/Z int divider = (rect.ymin + rect.ymax)/2; splitIndex = SplitByZ(nodeBounds, permutation, from, to, divider); } int margin = (to - from)/8; bool veryUneven = splitIndex <= from + margin || splitIndex >= to - margin; if (veryUneven) { // All nodes were on one side of the divider // Try to split along the other axis if (!odd) { // X int divider = (rect.xmin + rect.xmax)/2; splitIndex = SplitByX(nodeBounds, permutation, from, to, divider); } else { // Y/Z int divider = (rect.ymin + rect.ymax)/2; splitIndex = SplitByZ(nodeBounds, permutation, from, to, divider); } veryUneven = splitIndex <= from + margin || splitIndex >= to - margin; if (veryUneven) { // Almost all nodes were on one side of the divider // Just pick one half splitIndex = (from+to)/2; } } var left = BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, from, splitIndex, !odd, depth+1); var right = BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, splitIndex, to, !odd, depth+1); var box = tree[boxId]; box.left = left; box.right = right; tree[boxId] = box; return boxId; } } /// Calculates the bounding box in XZ space of all nodes between from (inclusive) and to (exclusive) static IntRect NodeBounds (NativeArray permutation, NativeArray nodeBounds, int from, int to) { var mn = nodeBounds[permutation[from]].Min.ToInt2(); var mx = nodeBounds[permutation[from]].Max.ToInt2(); for (int j = from + 1; j < to; j++) { var otherRect = nodeBounds[permutation[j]]; var rmin = new int2(otherRect.xmin, otherRect.ymin); var rmax = new int2(otherRect.xmax, otherRect.ymax); mn = math.min(mn, rmin); mx = math.max(mx, rmax); } return new IntRect(mn.x, mn.y, mx.x, mx.y); } [BurstCompile] public readonly struct ProjectionParams { public readonly float2x3 planeProjection; public readonly float2 projectedUpNormalized; public readonly float3 projectionAxis; public readonly float distanceScaleAlongProjectionAxis; public readonly DistanceMetric distanceMetric; // bools are for some reason not blittable by the burst compiler, so we have to use a byte readonly byte alignedWithXZPlaneBacking; public bool alignedWithXZPlane => alignedWithXZPlaneBacking != 0; /// /// Calculates the squared distance from a point to a box when projected to 2D. /// /// The input rectangle is assumed to be on the XZ plane, and to actually represent an infinitely tall box (along the Y axis). /// /// The planeProjection matrix projects points from 3D to 2D. The box will also be projected. /// The upProjNormalized vector is the normalized direction orthogonal to the 2D projection. /// It is the direction pointing out of the plane from the projection's point of view. /// /// In the special case that the projection just projects 3D coordinates onto the XZ plane, this is /// equivalent to the distance from a point to a rectangle in 2D. /// public float SquaredRectPointDistanceOnPlane (IntRect rect, float3 p) { return SquaredRectPointDistanceOnPlane(in this, ref rect, ref p); } [BurstCompile(FloatMode = FloatMode.Fast)][IgnoredByDeepProfiler] private static float SquaredRectPointDistanceOnPlane (in ProjectionParams projection, ref IntRect rect, ref float3 p) { if (projection.alignedWithXZPlane) { var p1 = new float2(rect.xmin, rect.ymin) * Int3.PrecisionFactor; var p4 = new float2(rect.xmax, rect.ymax) * Int3.PrecisionFactor; var closest = math.clamp(p.xz, p1, p4); return math.lengthsq(closest - p.xz); } else { var p1 = new float3(rect.xmin, 0, rect.ymin) * Int3.PrecisionFactor - p; var p4 = new float3(rect.xmax, 0, rect.ymax) * Int3.PrecisionFactor - p; var p2 = new float3(rect.xmin, 0, rect.ymax) * Int3.PrecisionFactor - p; var p3 = new float3(rect.xmax, 0, rect.ymin) * Int3.PrecisionFactor - p; var p1proj = math.mul(projection.planeProjection, p1); var p2proj = math.mul(projection.planeProjection, p2); var p3proj = math.mul(projection.planeProjection, p3); var p4proj = math.mul(projection.planeProjection, p4); var upNormal = new float2(projection.projectedUpNormalized.y, -projection.projectedUpNormalized.x); // Calculate the dot product of pNproj and upNormal for all N, this is the distance between p and pN // along the direction orthogonal to upProjNormalized. // The box is infinite along the up direction (since it is only a rect). When projected down to 2D // this results in an infinite line with a given thickness (a beam). // This is assuming the projection direction is not parallel to the world up direction, in which case we // would have entered the other branch of this if statement. // The minumum value and maximum value in dists gives us the signed distance to this beam // from the point p. var dists = math.mul(math.transpose(new float2x4(p1proj, p2proj, p3proj, p4proj)), upNormal); // Calculate the shortest distance to the beam (may be 0 if p is inside the beam). var dist = math.clamp(0, math.cmin(dists), math.cmax(dists)); return dist*dist; } } public ProjectionParams(NNConstraint constraint, GraphTransform graphTransform) { const float MAX_ERROR_IN_RADIANS = 0.01f; // The normal of the plane we are projecting onto (if any). if (constraint != null && constraint.distanceMetric.projectionAxis != Vector3.zero) { // (inf,inf,inf) is a special value indicating to use the graph's natural up direction if (float.IsPositiveInfinity(constraint.distanceMetric.projectionAxis.x)) { projectionAxis = new float3(0, 1, 0); } else { projectionAxis = math.normalizesafe(graphTransform.InverseTransformVector(constraint.distanceMetric.projectionAxis)); } if (projectionAxis.x*projectionAxis.x + projectionAxis.z*projectionAxis.z < MAX_ERROR_IN_RADIANS*MAX_ERROR_IN_RADIANS) { // We could let the code below handle this case, but since it is a common case we can optimize it a bit // by using a fast-path here. projectedUpNormalized = float2.zero; planeProjection = new float2x3(1, 0, 0, 0, 0, 1); // math.transpose(new float3x2(new float3(1, 0, 0), new float3(0, 0, 1))); distanceMetric = DistanceMetric.ScaledManhattan; alignedWithXZPlaneBacking = (byte)1; distanceScaleAlongProjectionAxis = math.max(constraint.distanceMetric.distanceScaleAlongProjectionDirection, 0); return; } // Find any two vectors which are perpendicular to the normal (and each other) var planeAxis1 = math.normalizesafe(math.cross(new float3(1, 0, 1), projectionAxis)); if (math.all(planeAxis1 == 0)) planeAxis1 = math.normalizesafe(math.cross(new float3(-1, 0, 1), projectionAxis)); var planeAxis2 = math.normalizesafe(math.cross(projectionAxis, planeAxis1)); // Note: The inverse of an orthogonal matrix is its transpose, and the transpose is faster to compute planeProjection = math.transpose(new float3x2(planeAxis1, planeAxis2)); // The projection of the (0,1,0) vector onto the plane. // This is important because the BBTree stores its rectangles in the XZ plane. // If the projection is close enough to the XZ plane, we snap to that because it allows us to use faster and more precise distance calculations. projectedUpNormalized = math.lengthsq(planeProjection.c1) <= MAX_ERROR_IN_RADIANS*MAX_ERROR_IN_RADIANS ? float2.zero : math.normalize(planeProjection.c1); distanceMetric = DistanceMetric.ScaledManhattan; alignedWithXZPlaneBacking = math.all(projectedUpNormalized == 0) ? (byte)1 : (byte)0; // The distance along the projection axis is scaled by a cost factor to make the distance // along the projection direction more or less important compared to the distance in the plane. // Usually the projection direction is less important. // For example, when an agent looks for the closest node, it is typically more interested in finding a point close // to it which is more or less directly below it, than it is in finding a point which is closer, but requires sideways movement. // Even if this value is zero we will use the distance along the projection axis to break ties. // Otherwise, when getting the nearest node in e.g. a tall building, it would not be well defined // which floor of the building was closest. distanceScaleAlongProjectionAxis = math.max(constraint.distanceMetric.distanceScaleAlongProjectionDirection, 0); } else { projectionAxis = float3.zero; planeProjection = default; projectedUpNormalized = default; distanceMetric = DistanceMetric.Euclidean; alignedWithXZPlaneBacking = 1; distanceScaleAlongProjectionAxis = 0; } } } public float DistanceSqrLowerBound (float3 p, in ProjectionParams projection) { if (tree.Length == 0) return float.PositiveInfinity; return projection.SquaredRectPointDistanceOnPlane(tree[0].rect, p); } /// /// Queries the tree for the closest node to p constrained by the NNConstraint trying to improve an existing solution. /// Note that this function will only fill in the constrained node. /// If you want a node not constrained by any NNConstraint, do an additional search with constraint = NNConstraint.None /// /// Point to search around /// Optionally set to constrain which nodes to return /// The best squared distance for the previous solution. Will be updated with the best distance /// after this search. Supply positive infinity to start the search from scratch. /// This search will start from the previous NNInfo and improve it if possible. Will be updated with the new result. /// Even if the search fails on this call, the solution will never be worse than previous. /// The nodes what this BBTree was built from /// The triangles that this BBTree was built from /// The vertices that this BBTree was built from /// Projection parameters derived from the constraint public void QueryClosest (float3 p, NNConstraint constraint, in ProjectionParams projection, ref float distanceSqr, ref NNInfo previous, GraphNode[] nodes, UnsafeSpan triangles, UnsafeSpan vertices) { if (tree.Length == 0) return; UnsafeSpan stack; unsafe { NearbyNodesIterator.BoxWithDist* stackPtr = stackalloc NearbyNodesIterator.BoxWithDist[MAX_TREE_HEIGHT]; stack = new UnsafeSpan(stackPtr, MAX_TREE_HEIGHT); } stack[0] = new NearbyNodesIterator.BoxWithDist { index = 0, distSqr = 0.0f, }; var it = new NearbyNodesIterator { stack = stack, stackSize = 1, indexInLeaf = 0, point = p, projection = projection, distanceThresholdSqr = distanceSqr, tieBreakingDistanceThreshold = float.PositiveInfinity, tree = tree.AsUnsafeSpan(), nodes = nodePermutation.AsUnsafeSpan(), triangles = triangles, vertices = vertices, }; // We use an iterator which searches through the tree and returns nodes closer than it.distanceThresholdSqr. // The iterator is compiled using burst for high performance, but when a new candidate node is found we need // to evaluate it in pure C# due to the NNConstraint being a C# class. // TODO: If constraint==null (or NNConstraint.None) we could run the whole thing in burst to improve perf even more. var result = previous; while (it.stackSize > 0 && it.MoveNext()) { var current = it.current; if (constraint == null || constraint.Suitable(nodes[current.node])) { it.distanceThresholdSqr = current.distanceSq; it.tieBreakingDistanceThreshold = current.tieBreakingDistance; result = new NNInfo(nodes[current.node], current.closestPointOnNode, current.distanceSq); } } distanceSqr = it.distanceThresholdSqr; previous = result; } struct CloseNode { public int node; public float distanceSq; public float tieBreakingDistance; public float3 closestPointOnNode; } public enum DistanceMetric: byte { Euclidean, ScaledManhattan, } [BurstCompile] struct NearbyNodesIterator : IEnumerator { public UnsafeSpan stack; public int stackSize; public UnsafeSpan tree; public UnsafeSpan nodes; public UnsafeSpan triangles; public UnsafeSpan vertices; public int indexInLeaf; public float3 point; public ProjectionParams projection; public float distanceThresholdSqr; public float tieBreakingDistanceThreshold; internal CloseNode current; public CloseNode Current => current; public struct BoxWithDist { public int index; public float distSqr; } public bool MoveNext () { return MoveNext(ref this); } void IDisposable.Dispose () {} void System.Collections.IEnumerator.Reset() => throw new NotSupportedException(); object System.Collections.IEnumerator.Current => throw new NotSupportedException(); // Note: Using FloatMode=Fast here can cause NaNs in rare cases. // I have not tracked down why, but it is not unreasonable given that FloatMode=Fast assumes that infinities do not happen. [BurstCompile(FloatMode = FloatMode.Default)] static bool MoveNext (ref NearbyNodesIterator it) { var distanceThresholdSqr = it.distanceThresholdSqr; while (true) { if (it.stackSize == 0) { return false; } // Pop the last element from the stack var boxRef = it.stack[it.stackSize-1]; // If we cannot possibly find anything better than the current best solution in here, skip this box. // Allow the search when we can find an equally close node, because tie breaking // may cause this search to find a better node. if (boxRef.distSqr > distanceThresholdSqr) { it.stackSize--; // Setting this to zero shouldn't be necessary in theory, as a leaf will always (in theory) be searched completely. // However, in practice the distance to a node may be a tiny bit lower than the distance to the box containing the node, due to floating point errors. // and so the leaf's search may be terminated early if a point is found on a node exactly on the border of the box. // In that case it is important that we reset the iterator to the start of the next leaf. it.indexInLeaf = 0; continue; } BBTreeBox box = it.tree[boxRef.index]; if (box.IsLeaf) { for (int i = it.indexInLeaf; i < MaximumLeafSize; i++) { var node = it.nodes[box.nodeOffset + i]; if (node == -1) break; var ti1 = (uint)(node*3 + 0); var ti2 = (uint)(node*3 + 1); var ti3 = (uint)(node*3 + 2); if (ti3 >= it.triangles.length) throw new Exception("Invalid node index"); Unity.Burst.CompilerServices.Hint.Assume(ti1 < it.triangles.length && ti2 < it.triangles.length && ti3 < it.triangles.length); var vi1 = it.vertices[it.triangles[ti1]]; var vi2 = it.vertices[it.triangles[ti2]]; var vi3 = it.vertices[it.triangles[ti3]]; if (it.projection.distanceMetric == DistanceMetric.Euclidean) { var v1 = (float3)vi1; var v2 = (float3)vi2; var v3 = (float3)vi3; Polygon.ClosestPointOnTriangleByRef(in v1, in v2, in v3, in it.point, out var closest); var sqrDist = math.distancesq(closest, it.point); if (sqrDist < distanceThresholdSqr) { it.indexInLeaf = i + 1; it.current = new CloseNode { node = node, distanceSq = sqrDist, tieBreakingDistance = 0, closestPointOnNode = closest, }; return true; } } else { Polygon.ClosestPointOnTriangleProjected(ref vi1, ref vi2, ref vi3, ref it.projection, ref it.point, out var closest, out var sqrDist, out var distAlongProjection); // Check if this point is better than the previously best point. // Handling ties here is important, in case the navmesh has multiple overlapping regions (e.g. a multi-story building). if (sqrDist < distanceThresholdSqr || (sqrDist == distanceThresholdSqr && distAlongProjection < it.tieBreakingDistanceThreshold)) { it.indexInLeaf = i + 1; it.current = new CloseNode { node = node, distanceSq = sqrDist, tieBreakingDistance = distAlongProjection, closestPointOnNode = closest, }; return true; } } } it.indexInLeaf = 0; it.stackSize--; } else { it.stackSize--; int first = box.left, second = box.right; var firstDist = it.projection.SquaredRectPointDistanceOnPlane(it.tree[first].rect, it.point); var secondDist = it.projection.SquaredRectPointDistanceOnPlane(it.tree[second].rect, it.point); if (secondDist < firstDist) { // Swap Memory.Swap(ref first, ref second); Memory.Swap(ref firstDist, ref secondDist); } if (it.stackSize + 2 > it.stack.Length) { throw new InvalidOperationException("Tree is too deep. Overflowed the internal stack."); } // Push both children on the stack so that we can explore them later (if they are not too far away). // We push the one with the smallest distance last so that it will be popped first. if (secondDist <= distanceThresholdSqr) it.stack[it.stackSize++] = new BoxWithDist { index = second, distSqr = secondDist, }; if (firstDist <= distanceThresholdSqr) it.stack[it.stackSize++] = new BoxWithDist { index = first, distSqr = firstDist, }; } } } } struct BBTreeBox { public IntRect rect; public int nodeOffset; public int left, right; public bool IsLeaf => nodeOffset >= 0; public BBTreeBox (IntRect rect) { nodeOffset = -1; this.rect = rect; left = right = -1; } } public void DrawGizmos (CommandBuilder draw) { Gizmos.color = new Color(1, 1, 1, 0.5F); if (tree.Length == 0) return; DrawGizmos(ref draw, 0, 0); } void DrawGizmos (ref CommandBuilder draw, int boxi, int depth) { BBTreeBox box = tree[boxi]; var min = (Vector3) new Int3(box.rect.xmin, 0, box.rect.ymin); var max = (Vector3) new Int3(box.rect.xmax, 0, box.rect.ymax); Vector3 center = (min+max)*0.5F; Vector3 size = max-min; size = new Vector3(size.x, 1, size.z); center.y += depth * 2; draw.xz.WireRectangle(center, new float2(size.x, size.z), AstarMath.IntToColor(depth, 1f)); if (!box.IsLeaf) { DrawGizmos(ref draw, box.left, depth + 1); DrawGizmos(ref draw, box.right, depth + 1); } } } }