885 lines
38 KiB
C#

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;
/// <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.
// 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));
}
/// <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;
}
}
}