using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using UnityEngine.Assertions; namespace Pathfinding { using Pathfinding.Pooling; using Pathfinding.Collections; using Unity.Burst; using Unity.Collections; using Unity.Mathematics; using UnityEngine.Profiling; /// /// 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 component. /// /// /// 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); /// } /// } /// } /// } /// } /// /// /// 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] /// [BurstCompile] public static class Funnel { /// Funnel in which the path to the target will be public struct FunnelPortals { public List left; public List right; } /// The type of a public enum PartType { /// An off-mesh link between two nodes in the same or different graphs OffMeshLink, /// A sequence of adjacent nodes in the same graph NodeSequence, } /// /// Part of a path. /// This is either a sequence of adjacent triangles /// or a link. /// See: NodeLink2 /// public struct PathPart { /// Index of the first node in this part public int startIndex; /// Index of the last node in this part public int endIndex; /// Exact start-point of this part or off-mesh link public Vector3 startPoint; /// Exact end-point of this part or off-mesh link public Vector3 endPoint; /// If this is an off-mesh link or a sequence of nodes in a single graph public PartType type; } /// Splits the path into a sequence of parts which are either off-mesh links or sequences of adjacent triangles public static List SplitIntoParts (Path path) { var nodes = path.path; var result = ListPool.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 parts, ref List nodes) { List resultNodes = ListPool.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.Release(ref nodes); nodes = resultNodes; } /// /// 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 /// public static void Simplify (PathPart part, IRaycastableGraph graph, List nodes, List 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 nodes, PathPart part) { if (nodes == null || nodes.Count == 0) { return new FunnelPortals { left = ListPool.Claim(0), right = ListPool.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.Claim(nodes.Count+1); var right = ListPool.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 { /// Left side of the funnel public NativeCircularBuffer leftFunnel; /// Right side of the funnel public NativeCircularBuffer rightFunnel; /// /// 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. /// public NativeCircularBuffer unwrappedPortals; /// /// 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). /// public float3 projectionAxis; public FunnelState(int initialCapacity, Allocator allocator) { leftFunnel = new NativeCircularBuffer(initialCapacity, allocator); rightFunnel = new NativeCircularBuffer(initialCapacity, allocator); unwrappedPortals = new NativeCircularBuffer(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); } /// True if a and b lie on different sides of the infinite line that passes through start and end 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; } /// /// 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). /// 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); } /// Like but for the end of the funnel 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 leftPortals, ref NativeCircularBuffer rightPortals, ref NativeCircularBuffer 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 newLeftPortal, List 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(); } /// /// 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 /// /// The maximum number of corners to add to the result array. Should be positive. /// 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. /// Start point of the funnel. The agent will move from here to the best point along the first portal. /// End point of the funnel. /// 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). public int CalculateNextCornerIndices (int maxCorners, NativeArray 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 result) { var indices = new NativeArray(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 indices, int numCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList 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 indices, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList 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 indices, bool splitAtEveryPortal, in float3 startPoint, in float3 endPoint, bool lastCorner, in float3 projectionAxis, ref UnsafeSpan 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 leftPortals, NativeCircularBuffer rightPortals, NativeCircularBuffer unwrappedPortals, float2 from, float2 to, NativeList 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. // This branch is extremely well predicted, since it will always be true for grid graphs, and always false for other graphs. if (!math.all(projectionAxis == 0)) { leftPortal -= projectionAxis * math.dot(leftPortal, projectionAxis); rightPortal -= projectionAxis * math.dot(rightPortal, projectionAxis); point -= projectionAxis * 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)); } /// True if b is to the right of or on the line from (0,0) to a private static bool RightOrColinear (Vector2 a, Vector2 b) { return (a.x*b.y - b.x*a.y) <= 0; } /// True if b is to the left of or on the line from (0,0) to a private static bool LeftOrColinear (Vector2 a, Vector2 b) { return (a.x*b.y - b.x*a.y) >= 0; } /// /// 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 /// /// The portals of the funnel. The first and last vertices portals must be single points (so for example left[0] == right[0]). /// 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). public static List 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(Allocator.Temp); state.CalculateNextCorners(int.MaxValue, splitAtEveryPortal, startPoint, endPoint, nativeResult); state.Dispose(); var result = ListPool.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; /// /// 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 /// /// Left side of the funnel. Should not contain the start point. /// Right side of the funnel. Should not contain the end point. /// 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. /// Start point of the funnel. The agent will move from here to the best point between leftPortals[0] and rightPortals[0]. /// End point of the funnel. /// 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. /// 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). /// 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. /// If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal. [BurstCompile] static unsafe int Calculate (ref NativeCircularBuffer unwrappedPortals, ref NativeCircularBuffer leftPortals, ref NativeCircularBuffer rightPortals, ref float3 startPoint, ref float3 endPoint, ref UnsafeSpan 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; } } }