summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Utilities/Funnel.cs
blob: 412b111d614930b614a3944adc7b34bc82dbeaa3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
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;
		}
	}
}