580 lines
24 KiB
C#

//#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;
/// <summary>
/// Axis Aligned Bounding Box Tree.
/// Holds a bounding box tree of triangles.
/// </summary>
[BurstCompile]
public struct BBTree : IDisposable {
/// <summary>Holds all tree nodes</summary>
UnsafeList<BBTreeBox> tree;
UnsafeList<int> 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();
}
/// <summary>Build a BBTree from a list of triangles.</summary>
/// <param name="triangles">The triangles. Each triplet of 3 indices represents a node. The triangles are assumed to be in clockwise order.</param>
/// <param name="vertices">The vertices of the triangles.</param>
public BBTree(UnsafeSpan<int> triangles, UnsafeSpan<Int3> 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<int> triangles, ref UnsafeSpan<Int3> vertices, out BBTree bbTree) {
var nodeCount = triangles.Length/3;
// We will use approximately 2N tree nodes
var tree = new UnsafeList<BBTreeBox>((int)(nodeCount * 2.1f), Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
// We will use approximately N node references
var nodes = new UnsafeList<int>((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<int>(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<IntRect>(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<IntRect> nodesBounds, NativeArray<int> 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<IntRect> nodesBounds, NativeArray<int> 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<int> permutation, NativeArray<IntRect> nodeBounds, ref UnsafeList<int> nodes, ref UnsafeList<BBTreeBox> 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;
}
}
/// <summary>Calculates the bounding box in XZ space of all nodes between from (inclusive) and to (exclusive)</summary>
static IntRect NodeBounds (NativeArray<int> permutation, NativeArray<IntRect> 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;
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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
/// </summary>
/// <param name="p">Point to search around</param>
/// <param name="constraint">Optionally set to constrain which nodes to return</param>
/// <param name="distanceSqr">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.</param>
/// <param name="previous">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.</param>
/// <param name="nodes">The nodes what this BBTree was built from</param>
/// <param name="triangles">The triangles that this BBTree was built from</param>
/// <param name="vertices">The vertices that this BBTree was built from</param>
/// <param name="projection">Projection parameters derived from the constraint</param>
public void QueryClosest (float3 p, NNConstraint constraint, in ProjectionParams projection, ref float distanceSqr, ref NNInfo previous, GraphNode[] nodes, UnsafeSpan<int> triangles, UnsafeSpan<Int3> vertices) {
if (tree.Length == 0) return;
UnsafeSpan<NearbyNodesIterator.BoxWithDist> stack;
unsafe {
NearbyNodesIterator.BoxWithDist* stackPtr = stackalloc NearbyNodesIterator.BoxWithDist[MAX_TREE_HEIGHT];
stack = new UnsafeSpan<NearbyNodesIterator.BoxWithDist>(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<CloseNode> {
public UnsafeSpan<BoxWithDist> stack;
public int stackSize;
public UnsafeSpan<BBTreeBox> tree;
public UnsafeSpan<int> nodes;
public UnsafeSpan<int> triangles;
public UnsafeSpan<Int3> 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);
}
}
}
}