diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs | 883 |
1 files changed, 883 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs new file mode 100644 index 0000000..412b111 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs @@ -0,0 +1,883 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Linq; +using UnityEngine.Assertions; + +namespace Pathfinding { + using Pathfinding.Util; + using Unity.Burst; + using Unity.Collections; + using Unity.Collections.LowLevel.Unsafe; + using Unity.Jobs; + using Unity.Mathematics; + using UnityEngine.Profiling; + + /// <summary> + /// Implements the funnel algorithm as well as various related methods. + /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html + /// See: Usually you do not use this class directly. Instead use the <see cref="FunnelModifier"/> component. + /// + /// <code> + /// using UnityEngine; + /// using Pathfinding; + /// using Pathfinding.Drawing; + /// + /// public class FunnelExample : MonoBehaviour { + /// public Transform target = null; + /// + /// void Update () { + /// var path = ABPath.Construct(transform.position, target.position); + /// + /// AstarPath.StartPath(path); + /// path.BlockUntilCalculated(); + /// + /// // Apply some default adjustments to the path + /// // not necessary if you are using the Seeker component + /// new StartEndModifier().Apply(path); + /// + /// // Split the path into segments and links + /// var parts = Funnel.SplitIntoParts(path); + /// // Optionally simplify the path to make it straighter + /// var nodes = path.path; + /// Funnel.Simplify(parts, ref nodes); + /// + /// using (Draw.WithLineWidth(2)) { + /// // Go through all the parts and draw them in the scene view + /// for (int i = 0; i < parts.Count; i++) { + /// var part = parts[i]; + /// if (part.type == Funnel.PartType.OffMeshLink) { + /// // Draw off-mesh links as a single line + /// Draw.Line(part.startPoint, part.endPoint, Color.cyan); + /// } else { + /// // Calculate the shortest path through the funnel + /// var portals = Funnel.ConstructFunnelPortals(nodes, part); + /// var pathThroghPortals = Funnel.Calculate(portals, splitAtEveryPortal: false); + /// Draw.Polyline(pathThroghPortals, Color.black); + /// } + /// } + /// } + /// } + /// } + /// </code> + /// + /// In the image you can see the output from the code example above. The cyan lines represent off-mesh links. + /// + /// [Open online documentation to see images] + /// </summary> + [BurstCompile] + public static class Funnel { + /// <summary>Funnel in which the path to the target will be</summary> + public struct FunnelPortals { + public List<Vector3> left; + public List<Vector3> right; + } + + /// <summary>The type of a <see cref="PathPart"/></summary> + public enum PartType { + /// <summary>An off-mesh link between two nodes in the same or different graphs</summary> + OffMeshLink, + /// <summary>A sequence of adjacent nodes in the same graph</summary> + NodeSequence, + } + + /// <summary> + /// Part of a path. + /// This is either a sequence of adjacent triangles + /// or a link. + /// See: NodeLink2 + /// </summary> + public struct PathPart { + /// <summary>Index of the first node in this part</summary> + public int startIndex; + /// <summary>Index of the last node in this part</summary> + public int endIndex; + /// <summary>Exact start-point of this part or off-mesh link</summary> + public Vector3 startPoint; + /// <summary>Exact end-point of this part or off-mesh link</summary> + public Vector3 endPoint; + /// <summary>If this is an off-mesh link or a sequence of nodes in a single graph</summary> + public PartType type; + } + + /// <summary>Splits the path into a sequence of parts which are either off-mesh links or sequences of adjacent triangles</summary> + + public static List<PathPart> SplitIntoParts (Path path) { + var nodes = path.path; + + var result = ListPool<PathPart>.Claim(); + + if (nodes == null || nodes.Count == 0) { + return result; + } + + // Loop through the path and split it into + // parts joined by links + for (int i = 0; i < nodes.Count; i++) { + var node = nodes[i]; + if (node is TriangleMeshNode || node is GridNodeBase) { + var startIndex = i; + uint currentGraphIndex = node.GraphIndex; + + // Loop up until we find a node in another graph + // Ignore NodeLink3 nodes + while (i < nodes.Count && (nodes[i].GraphIndex == currentGraphIndex || nodes[i] is NodeLink3Node)) i++; + + i--; + var endIndex = i; + result.Add(new PathPart { + type = PartType.NodeSequence, + startIndex = startIndex, + endIndex = endIndex, + // If this is the first part in the path, use the exact start point + // otherwise use the position of the node right before the start of this + // part which is likely the end of the link to this part + startPoint = startIndex == 0 ? path.vectorPath[0] : (Vector3)nodes[startIndex-1].position, + endPoint = endIndex == nodes.Count-1 ? path.vectorPath[path.vectorPath.Count-1] : (Vector3)nodes[endIndex+1].position, + }); + } else if (node is LinkNode) { + var startIndex = i; + var currentGraphIndex = node.GraphIndex; + + while (i < nodes.Count && nodes[i].GraphIndex == currentGraphIndex) i++; + i--; + + if (i - startIndex == 0) { + // The link is a single node. + // Just ignore it. It can happen in very rare circumstances with some path types. + // For example, a RandomPath can stop at the first node of a node link, without including the other end of the link. + + if (startIndex > 0 && startIndex + 1 < nodes.Count && nodes[startIndex - 1] == nodes[startIndex + 1]) { + // We can also move to a node link node and then immediately move back to the previous node in rare circumstances. + // Since triangle nodes are represented as 3 nodes during pathfinding, this is a possibility. + // (TODO: How can this happen in practice? It has been empirically observed on a standard graph, but the edge costs must be kinda weird for it to happen?) + + // [A, LinkNode, A] => [A] + nodes.RemoveRange(startIndex, 2); + i--; + throw new System.Exception("Link node connected back to the previous node in the path. This should not happen."); + } else { + // [A, LinkNode] => [A] + // [LinkNode, A] => [A] + Assert.IsTrue(startIndex == 0 || startIndex == nodes.Count - 1); + nodes.RemoveAt(startIndex); + i--; + } + + continue; + } else if (i - startIndex != 1) { + throw new System.Exception("Off mesh link included more than two nodes: " + (i - startIndex + 1)); + } + + result.Add(new PathPart { + type = PartType.OffMeshLink, + startIndex = startIndex, + endIndex = i, + startPoint = (Vector3)nodes[startIndex].position, + endPoint = (Vector3)nodes[i].position, + }); + } else { + throw new System.Exception("Unsupported node type or null node"); + } + } + + // The path should always start and stop on regular nodes + if (result[0].type == PartType.OffMeshLink) { + result.RemoveAt(0); + } + if (result[result.Count - 1].type == PartType.OffMeshLink) { + result.RemoveAt(result.Count - 1); + } + + Assert.IsTrue(result.Count > 0); + Assert.AreEqual(result[0].startIndex, 0); + Assert.AreEqual(result[0].type, PartType.NodeSequence); + Assert.AreEqual(result[result.Count-1].type, PartType.NodeSequence); + Assert.AreEqual(result[result.Count-1].endIndex, nodes.Count - 1); + + return result; + } + + + public static void Simplify (List<PathPart> parts, ref List<GraphNode> nodes) { + List<GraphNode> resultNodes = ListPool<GraphNode>.Claim(); + + for (int i = 0; i < parts.Count; i++) { + var part = parts[i]; + + // We are changing the nodes list, so indices may change + var newPart = part; + newPart.startIndex = resultNodes.Count; + + if (part.type == PartType.NodeSequence) { + if (nodes[part.startIndex].Graph is IRaycastableGraph graph) { + Simplify(part, graph, nodes, resultNodes, Path.ZeroTagPenalties, -1); + newPart.endIndex = resultNodes.Count - 1; + parts[i] = newPart; + continue; + } + } + + for (int j = part.startIndex; j <= part.endIndex; j++) { + resultNodes.Add(nodes[j]); + } + newPart.endIndex = resultNodes.Count - 1; + parts[i] = newPart; + } + + ListPool<GraphNode>.Release(ref nodes); + nodes = resultNodes; + } + + /// <summary> + /// Simplifies a funnel path using linecasting. + /// Running time is roughly O(n^2 log n) in the worst case (where n = end-start) + /// Actually it depends on how the graph looks, so in theory the actual upper limit on the worst case running time is O(n*m log n) (where n = end-start and m = nodes in the graph) + /// but O(n^2 log n) is a much more realistic worst case limit. + /// + /// Requires graph to implement IRaycastableGraph + /// </summary> + public static void Simplify (PathPart part, IRaycastableGraph graph, List<GraphNode> nodes, List<GraphNode> result, int[] tagPenalties, int traversableTags) { + var start = part.startIndex; + var end = part.endIndex; + var startPoint = part.startPoint; + var endPoint = part.endPoint; + + if (graph == null) throw new System.ArgumentNullException(nameof(graph)); + + if (start > end) { + throw new System.ArgumentException("start > end"); + } + + // Do a straight line of sight check to see if the path can be simplified to a single line + { + if (!graph.Linecast(startPoint, endPoint, out GraphHitInfo hit) && hit.node == nodes[end]) { + graph.Linecast(startPoint, endPoint, out hit, result); + + long penaltySum = 0; + long penaltySum2 = 0; + for (int i = start; i <= end; i++) { + penaltySum += nodes[i].Penalty + tagPenalties[nodes[i].Tag]; + } + + bool walkable = true; + for (int i = 0; i < result.Count; i++) { + penaltySum2 += result[i].Penalty + tagPenalties[result[i].Tag]; + walkable &= ((traversableTags >> (int)result[i].Tag) & 1) == 1; + } + + // Allow 40% more penalty on average per node + if (!walkable || (penaltySum*1.4*result.Count) < (penaltySum2*(end-start+1))) { + // The straight line penalties are much higher than the original path. + // Revert the simplification + result.Clear(); + } else { + // The straight line simplification looks good. + // We are done here. + return; + } + } + } + + int ostart = start; + + int count = 0; + while (true) { + if (count++ > 1000) { + Debug.LogError("Was the path really long or have we got cought in an infinite loop?"); + break; + } + + if (start == end) { + result.Add(nodes[end]); + return; + } + + int resCount = result.Count; + + // Run a binary search to find the furthest node that we have a clear line of sight to + int mx = end+1; + int mn = start+1; + bool anySucceded = false; + while (mx > mn+1) { + int mid = (mx+mn)/2; + + Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position; + Vector3 ep = mid == end ? endPoint : (Vector3)nodes[mid].position; + + // Check if there is an obstacle between these points, or if there is no obstacle, but we didn't end up at the right node. + // The second case can happen for example in buildings with multiple floors. + if (graph.Linecast(sp, ep, out GraphHitInfo hit) || hit.node != nodes[mid]) { + mx = mid; + } else { + anySucceded = true; + mn = mid; + } + } + + if (!anySucceded) { + result.Add(nodes[start]); + + // It is guaranteed that mn = start+1 + start = mn; + } else { + // Replace a part of the path with the straight path to the furthest node we had line of sight to. + // Need to redo the linecast to get the trace (i.e. list of nodes along the line of sight). + Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position; + Vector3 ep = mn == end ? endPoint : (Vector3)nodes[mn].position; + graph.Linecast(sp, ep, out _, result); + + long penaltySum = 0; + long penaltySum2 = 0; + for (int i = start; i <= mn; i++) { + penaltySum += nodes[i].Penalty + tagPenalties[nodes[i].Tag]; + } + + bool walkable = true; + for (int i = resCount; i < result.Count; i++) { + penaltySum2 += result[i].Penalty + tagPenalties[result[i].Tag]; + walkable &= ((traversableTags >> (int)result[i].Tag) & 1) == 1; + } + + // Allow 40% more penalty on average per node + if (!walkable || (penaltySum*1.4*(result.Count-resCount)) < (penaltySum2*(mn-start+1)) || result[result.Count-1] != nodes[mn]) { + // Linecast hit the wrong node or it is a lot more expensive than the original path + result.RemoveRange(resCount, result.Count-resCount); + + result.Add(nodes[start]); + start++; + } else { + //Remove nodes[end] + result.RemoveAt(result.Count-1); + start = mn; + } + } + } + } + + public static FunnelPortals ConstructFunnelPortals (List<GraphNode> nodes, PathPart part) { + if (nodes == null || nodes.Count == 0) { + return new FunnelPortals { left = ListPool<Vector3>.Claim(0), right = ListPool<Vector3>.Claim(0) }; + } + + if (part.endIndex < part.startIndex || part.startIndex < 0 || part.endIndex > nodes.Count) throw new System.ArgumentOutOfRangeException(); + + // Claim temporary lists and try to find lists with a high capacity + var left = ListPool<Vector3>.Claim(nodes.Count+1); + var right = ListPool<Vector3>.Claim(nodes.Count+1); + + // Add start point + left.Add(part.startPoint); + right.Add(part.startPoint); + + // Loop through all nodes in the path (except the last one) + for (int i = part.startIndex; i < part.endIndex; i++) { + // Get the portal between path[i] and path[i+1] and add it to the left and right lists + if (nodes[i].GetPortal(nodes[i+1], out var lp, out var rp)) { + left.Add(lp); + right.Add(rp); + } else { + // Fallback, just use the positions of the nodes + left.Add((Vector3)nodes[i].position); + right.Add((Vector3)nodes[i].position); + + left.Add((Vector3)nodes[i+1].position); + right.Add((Vector3)nodes[i+1].position); + } + } + + // Add end point + left.Add(part.endPoint); + right.Add(part.endPoint); + + return new FunnelPortals { left = left, right = right }; + } + + [BurstCompile] + public struct FunnelState { + /// <summary>Left side of the funnel</summary> + public NativeCircularBuffer<float3> leftFunnel; + /// <summary>Right side of the funnel</summary> + public NativeCircularBuffer<float3> rightFunnel; + /// <summary> + /// Unwrapped version of the funnel portals in 2D space. + /// + /// The input is a funnel like in the image below. It may be rotated and twisted. + /// [Open online documentation to see images] + /// The output will be a funnel in 2D space like in the image below. All twists and bends will have been straightened out. + /// [Open online documentation to see images] + /// + /// This array is used as a cache and the unwrapped portals are calculated on demand. Thus it may not contain all portals. + /// </summary> + public NativeCircularBuffer<float4> unwrappedPortals; + + /// <summary> + /// If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal. + /// + /// This is used to make the funnel fit a rotated graph better. + /// It is ideally used for grid graphs, but navmesh/recast graphs are probably better off with it set to zero. + /// + /// The vector should be normalized (unless zero), in world space, and should never be changed after the first portal has been added (unless the funnel is cleared first). + /// </summary> + public float3 projectionAxis; + + + public FunnelState(int initialCapacity, Allocator allocator) { + leftFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator); + rightFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator); + unwrappedPortals = new NativeCircularBuffer<float4>(initialCapacity, allocator); + projectionAxis = float3.zero; + } + + public FunnelState(FunnelPortals portals, Allocator allocator) : this(portals.left.Count, allocator) { + if (portals.left.Count != portals.right.Count) throw new System.ArgumentException("portals.left.Count != portals.right.Count"); + for (int i = 0; i < portals.left.Count; i++) { + PushEnd(portals.left[i], portals.right[i]); + } + } + + public FunnelState Clone () { + return new FunnelState { + leftFunnel = leftFunnel.Clone(), + rightFunnel = rightFunnel.Clone(), + unwrappedPortals = unwrappedPortals.Clone(), + projectionAxis = projectionAxis, + }; + } + + public void Clear () { + leftFunnel.Clear(); + rightFunnel.Clear(); + unwrappedPortals.Clear(); + projectionAxis = float3.zero; + } + + public void PopStart () { + leftFunnel.PopStart(); + rightFunnel.PopStart(); + if (unwrappedPortals.Length > 0) unwrappedPortals.PopStart(); + } + + public void PopEnd () { + leftFunnel.PopEnd(); + rightFunnel.PopEnd(); + unwrappedPortals.TrimTo(leftFunnel.Length); + } + + public void Pop (bool fromStart) { + if (fromStart) PopStart(); + else PopEnd(); + } + + public void PushStart (float3 newLeftPortal, float3 newRightPortal) { + PushStart(ref leftFunnel, ref rightFunnel, ref unwrappedPortals, ref newLeftPortal, ref newRightPortal, ref projectionAxis); + } + + /// <summary>True if a and b lie on different sides of the infinite line that passes through start and end</summary> + static bool DifferentSidesOfLine (float3 start, float3 end, float3 a, float3 b) { + var portal = math.normalizesafe(end - start); + var d1 = a - start; + var d2 = b - start; + d1 -= portal * math.dot(d1, portal); + d2 -= portal * math.dot(d2, portal); + return math.dot(d1, d2) < 0; + } + + /// <summary> + /// True if it is reasonable that the given start point has passed the first portal in the funnel. + /// + /// If this is true, it is most likely better to pop the start/end portal of the funnel first. + /// + /// This can be used as a heuristic to determine if the agent has passed a portal and we should pop it, + /// in those cases when node information is not available (e.g. because the path has been invalidated). + /// </summary> + public bool IsReasonableToPopStart (float3 startPoint, float3 endPoint) { + if (leftFunnel.Length == 0) return false; + + var reference = 1; + while (reference < leftFunnel.Length && VectorMath.IsColinear(leftFunnel.First, rightFunnel.First, leftFunnel[reference])) { + reference++; + } + return !DifferentSidesOfLine(leftFunnel.First, rightFunnel.First, startPoint, reference < leftFunnel.Length ? leftFunnel[reference] : endPoint); + } + + /// <summary>Like <see cref="IsReasonableToPopStart"/> but for the end of the funnel</summary> + public bool IsReasonableToPopEnd (float3 startPoint, float3 endPoint) { + if (leftFunnel.Length == 0) return false; + + var reference = leftFunnel.Length - 1; + while (reference >= 0 && VectorMath.IsColinear(leftFunnel.Last, rightFunnel.Last, leftFunnel[reference])) { + reference--; + } + return !DifferentSidesOfLine(leftFunnel.Last, rightFunnel.Last, endPoint, reference >= 0 ? leftFunnel[reference] : startPoint); + } + + [BurstCompile] + static void PushStart (ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref NativeCircularBuffer<float4> unwrappedPortals, ref float3 newLeftPortal, ref float3 newRightPortal, ref float3 projectionAxis) { + if (unwrappedPortals.Length == 0) { + leftPortals.PushStart(newLeftPortal); + rightPortals.PushStart(newRightPortal); + return; + } + + var firstUnwrapped = unwrappedPortals.First; + var unwrappedRight = Unwrap(leftPortals.First, rightPortals.First, firstUnwrapped.xy, firstUnwrapped.zw, newRightPortal, -1, projectionAxis); + var unwrappedLeft = Unwrap(leftPortals.First, newRightPortal, firstUnwrapped.xy, unwrappedRight, newLeftPortal, -1, projectionAxis); + leftPortals.PushStart(newLeftPortal); + rightPortals.PushStart(newRightPortal); + unwrappedPortals.PushStart(new float4(unwrappedLeft, unwrappedRight)); + } + + public void Splice (int startIndex, int toRemove, List<float3> newLeftPortal, List<float3> newRightPortal) { + this.leftFunnel.Splice(startIndex, toRemove, newLeftPortal); + this.rightFunnel.Splice(startIndex, toRemove, newRightPortal); + unwrappedPortals.TrimTo(startIndex); + } + + public void PushEnd (Vector3 newLeftPortal, Vector3 newRightPortal) { + leftFunnel.PushEnd(newLeftPortal); + rightFunnel.PushEnd(newRightPortal); + } + + public void Push (bool toStart, Vector3 newLeftPortal, Vector3 newRightPortal) { + if (toStart) PushStart(newLeftPortal, newRightPortal); + else PushEnd(newLeftPortal, newRightPortal); + } + + public void Dispose () { + leftFunnel.Dispose(); + rightFunnel.Dispose(); + unwrappedPortals.Dispose(); + } + + /// <summary> + /// Calculate the shortest path through the funnel. + /// + /// Returns: The number of corners added to the result array. + /// + /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html + /// </summary> + /// <param name="maxCorners">The maximum number of corners to add to the result array. Should be positive.</param> + /// <param name="result">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index of the left or right portals, depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param> + /// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point along the first portal.</param> + /// <param name="endPoint">End point of the funnel.</param> + /// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param> + public int CalculateNextCornerIndices (int maxCorners, NativeArray<int> result, float3 startPoint, float3 endPoint, out bool lastCorner) { + Assert.AreEqual(leftFunnel.Length, rightFunnel.Length); + Assert.IsTrue(unwrappedPortals.Length <= leftFunnel.Length); + if (result.Length < math.min(maxCorners, leftFunnel.Length)) throw new System.ArgumentException("result array may not be large enough to hold all corners"); + + unsafe { + // TODO: Pass this as ref instead? + var resultsSpan = result.AsUnsafeSpan(); + return Calculate(ref unwrappedPortals, ref leftFunnel, ref rightFunnel, ref startPoint, ref endPoint, ref resultsSpan, maxCorners, ref projectionAxis, out lastCorner); + } + } + + public void CalculateNextCorners (int maxCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, NativeList<float3> result) { + var indices = new NativeArray<int>(math.min(maxCorners, leftFunnel.Length), Allocator.Temp); + var numCorners = CalculateNextCornerIndices(maxCorners, indices, startPoint, endPoint, out bool lastCorner); + ConvertCornerIndicesToPath(indices, numCorners, splitAtEveryPortal, startPoint, endPoint, lastCorner, result); + indices.Dispose(); + } + + public void ConvertCornerIndicesToPath (NativeArray<int> indices, int numCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result) { + if (result.Capacity < numCorners) result.Capacity = numCorners; + + Assert.IsTrue(numCorners == 0 || (indices[numCorners-1] & FunnelPortalIndexMask) < unwrappedPortals.Length); + result.Add(startPoint); + if (leftFunnel.Length == 0) { + if (lastCorner) result.Add(endPoint); + return; + } + + if (splitAtEveryPortal) { + float2 prev2D = Unwrap(leftFunnel[0], rightFunnel[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis); + var prevIdx = 0; + for (int i = 0; i < numCorners; i++) { + var idx = indices[i] & FunnelPortalIndexMask; + var rightSide = (indices[i] & RightSideBit) != 0; + // Check intersections with every portal segment + float2 next2D = rightSide ? unwrappedPortals[idx].zw : unwrappedPortals[idx].xy; + CalculatePortalIntersections(prevIdx + 1, idx - 1, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result); + prevIdx = math.abs(idx); + prev2D = next2D; + + result.Add(rightSide ? rightFunnel[idx] : leftFunnel[idx]); + } + if (lastCorner) { + var next2D = Unwrap(leftFunnel.Last, rightFunnel.Last, unwrappedPortals.Last.xy, unwrappedPortals.Last.zw, endPoint, 1, projectionAxis); + CalculatePortalIntersections(prevIdx + 1, unwrappedPortals.Length - 1, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result); + result.Add(endPoint); + } + } else { + for (int i = 0; i < numCorners; i++) { + var idx = indices[i]; + result.Add((idx & RightSideBit) != 0 ? rightFunnel[idx & FunnelPortalIndexMask] : leftFunnel[idx & FunnelPortalIndexMask]); + } + if (lastCorner) result.Add(endPoint); + } + } + + public void ConvertCornerIndicesToPathProjected (UnsafeSpan<int> indices, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result, float3 up) { + var resultCount = indices.Length + 1 + (lastCorner ? 1 : 0); + if (result.Capacity < resultCount) result.Capacity = resultCount; + result.ResizeUninitialized(resultCount); + var resultSpan = result.AsUnsafeSpan(); + ConvertCornerIndicesToPathProjected(ref this, ref indices, splitAtEveryPortal, in startPoint, in endPoint, lastCorner, in projectionAxis, ref resultSpan, in up); + } + + public float4x3 UnwrappedPortalsToWorldMatrix (float3 up) { + int startIndex = 0; + while (startIndex < unwrappedPortals.Length && math.lengthsq(unwrappedPortals[startIndex].xy - unwrappedPortals[startIndex].zw) <= 0.00001f) startIndex++; + if (startIndex >= unwrappedPortals.Length) return new float4x3(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1); + var left2D = unwrappedPortals[startIndex].xy; + var right2D = unwrappedPortals[startIndex].zw; + var left3D = leftFunnel[startIndex]; + var right3D = rightFunnel[startIndex]; + var portal2D = right2D - left2D; + var portal3D = right3D - left3D; + var portal2DInv = portal2D * math.rcp(math.lengthsq(portal2D)); + // Matrix to rotate unwrapped portals so that portal2D maps to the x-axis (1,0) + var mr = new float2x2( + new float2(portal2DInv.x, -portal2DInv.y), + new float2(portal2DInv.y, portal2DInv.x) + ); + + // Matrix to transform points in unwrapped-portal-space so left2D maps to (0,0) and right2D maps to (1,0) + var offset = math.mul(mr, -left2D); + var m1 = new float4x3( + new float4(mr.c0.x, 0, mr.c0.y, 0), + new float4(mr.c1.x, 0, mr.c1.y, 0), + new float4(offset.x, 0, offset.y, 1) + ); + + // Matrix that maps (0,0,0) to left3D and (1,0,0) to right3D, as well as (0,1,0) to up. + var m2 = new float4x4( + new float4(portal3D, 0), + new float4(up, 0), + new float4(math.cross(portal3D, up), 0), + new float4(left3D, 1) + ); + + // Matrix to transform points in unwrapped-portal-space to 3D space. Such that left2D maps to left3D. + return math.mul(m2, m1); + } + + [BurstCompile] + public static void ConvertCornerIndicesToPathProjected (ref FunnelState funnelState, ref UnsafeSpan<int> indices, bool splitAtEveryPortal, in float3 startPoint, in float3 endPoint, bool lastCorner, in float3 projectionAxis, ref UnsafeSpan<float3> result, in float3 up) { + Assert.IsTrue(indices.Length == 0 || (indices[indices.Length-1] & FunnelPortalIndexMask) < funnelState.unwrappedPortals.Length); + int resultIndex = 0; + result[resultIndex++] = startPoint; + if (funnelState.leftFunnel.Length == 0) { + if (lastCorner) result[resultIndex++] = endPoint; + Assert.AreEqual(resultIndex, result.Length); + return; + } + + var unwrappedToWorld = funnelState.UnwrappedPortalsToWorldMatrix(up); + + if (splitAtEveryPortal) { + throw new System.NotImplementedException(); + } else { + for (int i = 0; i < indices.Length; i++) { + var idx = indices[i]; + var corner = (idx & RightSideBit) != 0 ? funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].zw : funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].xy; + result[resultIndex++] = math.mul(unwrappedToWorld, new float3(corner, 1)).xyz; + } + if (lastCorner) { + float2 endPoint2D = Unwrap(funnelState.leftFunnel.Last, funnelState.rightFunnel.Last, funnelState.unwrappedPortals.Last.xy, funnelState.unwrappedPortals.Last.zw, endPoint, 1, projectionAxis); + result[resultIndex++] = math.mul(unwrappedToWorld, new float3(endPoint2D, 1)).xyz; + } + } + Assert.AreEqual(resultIndex, result.Length); + } + + static void CalculatePortalIntersections (int startIndex, int endIndex, NativeCircularBuffer<float3> leftPortals, NativeCircularBuffer<float3> rightPortals, NativeCircularBuffer<float4> unwrappedPortals, float2 from, float2 to, NativeList<float3> result) { + for (int j = startIndex; j < endIndex; j++) { + var portal = unwrappedPortals[j]; + var left = portal.xy; + var right = portal.zw; + if (!VectorMath.LineLineIntersectionFactor(left, right - left, from, to - from, out float factor)) { + // This really shouldn't happen + factor = 0.5f; + } + result.Add(math.lerp(leftPortals[j], rightPortals[j], factor)); + } + } + } + + private static float2 Unwrap (float3 leftPortal, float3 rightPortal, float2 leftUnwrappedPortal, float2 rightUnwrappedPortal, float3 point, float sideMultiplier, float3 projectionAxis) { + // TODO: On grid graphs this is kind of a weird way to do it. + // We project all points onto a plane and then unwrap them. + // It would be faster (and possibly more numerically accurate) to transform the points to graph space and then just use the xz coordinates. + // We also slow down the navmesh case by adding these 3 dot products. + leftPortal -= math.dot(leftPortal, projectionAxis); + rightPortal -= math.dot(rightPortal, projectionAxis); + point -= math.dot(point, projectionAxis); + + var portal = rightPortal - leftPortal; + var portalLengthInvSq = 1.0f / math.lengthsq(portal); + if (float.IsPositiveInfinity(portalLengthInvSq)) { + return leftUnwrappedPortal + new float2(-math.length(point - leftPortal), 0); + } + var distance = math.length(math.cross(point - leftPortal, portal)) * portalLengthInvSq; + var projection = math.dot(point - leftPortal, portal) * portalLengthInvSq; + + // Weld corner vertices if they are close enough. + // This is important for grid graphs, as if the unwrapped portals are not quite identical in the corners, + // the grid simplification may fail to remove inner corners. This is because it will detect 2+ almost identical corners in each turn, instead of 1. + // TODO: Unwrap grid portals in a different way. It really can be done much faster and more numerically robustly. + // We should not use graph space directly, though, as grid graphs can move around (ProceduralGraphMover). + if (distance < 0.002f) { + if (math.abs(projection) < 0.002f) { + return leftUnwrappedPortal; + } else if (math.abs(projection - 1) < 0.002f) { + return rightUnwrappedPortal; + } + } + + var unwrappedPortal = rightUnwrappedPortal - leftUnwrappedPortal; + var unwrappedNormal = new float2(-unwrappedPortal.y, unwrappedPortal.x); + return leftUnwrappedPortal + math.mad(unwrappedPortal, projection, unwrappedNormal * (distance * sideMultiplier)); + } + + /// <summary>True if b is to the right of or on the line from (0,0) to a</summary> + private static bool RightOrColinear (Vector2 a, Vector2 b) { + return (a.x*b.y - b.x*a.y) <= 0; + } + + /// <summary>True if b is to the left of or on the line from (0,0) to a</summary> + private static bool LeftOrColinear (Vector2 a, Vector2 b) { + return (a.x*b.y - b.x*a.y) >= 0; + } + + /// <summary> + /// Calculate the shortest path through the funnel. + /// + /// The path will be unwrapped into 2D space before the funnel algorithm runs. + /// This makes it possible to support the funnel algorithm in XY space as well as in more complicated cases, such as on curved worlds. + /// [Open online documentation to see images] + /// + /// [Open online documentation to see images] + /// + /// See: Unwrap + /// </summary> + /// <param name="funnel">The portals of the funnel. The first and last vertices portals must be single points (so for example left[0] == right[0]).</param> + /// <param name="splitAtEveryPortal">If true, then a vertex will be inserted every time the path crosses a portal + /// instead of only at the corners of the path. The result will have exactly one vertex per portal if this is enabled. + /// This may introduce vertices with the same position in the output (esp. in corners where many portals meet).</param> + public static List<Vector3> Calculate (FunnelPortals funnel, bool splitAtEveryPortal) { + var state = new FunnelState(funnel, Allocator.Temp); + var startPoint = state.leftFunnel.First; + var endPoint = state.leftFunnel.Last; + state.PopStart(); + state.PopEnd(); + var nativeResult = new NativeList<float3>(Allocator.Temp); + state.CalculateNextCorners(int.MaxValue, splitAtEveryPortal, startPoint, endPoint, nativeResult); + state.Dispose(); + var result = ListPool<Vector3>.Claim(nativeResult.Length); + for (int i = 0; i < nativeResult.Length; i++) result.Add((Vector3)nativeResult[i]); + nativeResult.Dispose(); + return result; + } + + public const int RightSideBit = 1 << 30; + public const int FunnelPortalIndexMask = RightSideBit - 1; + + /// <summary> + /// Calculate the shortest path through the funnel. + /// + /// Returns: The number of corners added to the funnelPath array. + /// + /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html + /// </summary> + /// <param name="leftPortals">Left side of the funnel. Should not contain the start point.</param> + /// <param name="rightPortals">Right side of the funnel. Should not contain the end point.</param> + /// <param name="unwrappedPortals">Cache of unwrapped portal segments. This may be empty, but it will be filled with unwrapped portals and next time you run the algorithm it will be faster.</param> + /// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point between leftPortals[0] and rightPortals[0].</param> + /// <param name="endPoint">End point of the funnel.</param> + /// <param name="funnelPath">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index into leftPortals or rightPortals depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param> + /// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param> + /// <param name="maxCorners">The first N corners of the optimized path will be calculated. Calculating fewer corners is faster. Pass int.MaxValue if you want to calculate all corners.</param> + /// <param name="projectionAxis">If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal.</param> + [BurstCompile] + static unsafe int Calculate (ref NativeCircularBuffer<float4> unwrappedPortals, ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref float3 startPoint, ref float3 endPoint, ref UnsafeSpan<int> funnelPath, int maxCorners, ref float3 projectionAxis, out bool lastCorner) { + lastCorner = false; + if (leftPortals.Length <= 0) { + lastCorner = true; + return 0; + } + if (maxCorners <= 0) return 0; + + int apexIndex = 0; + int rightIndex = 0; + int leftIndex = 0; + + int outputCount = 0; + + if (unwrappedPortals.Length == 0) { + unwrappedPortals.PushEnd(new float4(new float2(0, 0), new float2(math.length(rightPortals[0] - leftPortals[0])))); + } + + float2 portalApex = Unwrap(leftPortals[0], rightPortals[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis); + float2 portalLeft = float2.zero; + float2 portalRight = float2.zero; + + for (int i = 0; i <= leftPortals.Length; i++) { + // Unwrap the funnel on the fly as needed + float2 rLeft, rRight; + if (i == unwrappedPortals.Length) { + if (i == leftPortals.Length) { + // The end point of the path + rLeft = rRight = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, endPoint, 1, projectionAxis) - portalApex; + } else { + // The funnel needs unwrapping + var unwrappedLeft = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, leftPortals[i], 1, projectionAxis); + var unwrappedRight = Unwrap(leftPortals[i], rightPortals[i-1], unwrappedLeft, unwrappedPortals[i-1].zw, rightPortals[i], 1, projectionAxis); + unwrappedPortals.PushEnd(new float4(unwrappedLeft, unwrappedRight)); + rLeft = unwrappedLeft - portalApex; + rRight = unwrappedRight - portalApex; + } + } else { + // Common case + rLeft = unwrappedPortals[i].xy - portalApex; + rRight = unwrappedPortals[i].zw - portalApex; + } + + if (LeftOrColinear(portalRight, rRight)) { + if (RightOrColinear(portalLeft, rRight)) { + portalRight = rRight; + rightIndex = i; + } else { + portalRight = portalLeft = float2.zero; + i = apexIndex = rightIndex = leftIndex; + portalApex = unwrappedPortals[i].xy; + + funnelPath[outputCount++] = apexIndex; + if (outputCount >= maxCorners) return outputCount; + continue; + } + } + + if (RightOrColinear(portalLeft, rLeft)) { + if (LeftOrColinear(portalRight, rLeft)) { + portalLeft = rLeft; + leftIndex = i; + } else { + portalRight = portalLeft = float2.zero; + i = apexIndex = leftIndex = rightIndex; + portalApex = unwrappedPortals[i].zw; + + funnelPath[outputCount++] = apexIndex | RightSideBit; + if (outputCount >= maxCorners) return outputCount; + continue; + } + } + } + + lastCorner = true; + return outputCount; + } + } +} |