Funnel.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Pathfinding {
  5. using Pathfinding.Util;
  6. /// <summary>
  7. /// Implements the funnel algorithm as well as various related methods.
  8. /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
  9. /// See: FunnelModifier for the component that you can attach to objects to use the funnel algorithm.
  10. /// </summary>
  11. public class Funnel {
  12. /// <summary>Funnel in which the path to the target will be</summary>
  13. public struct FunnelPortals {
  14. public List<Vector3> left;
  15. public List<Vector3> right;
  16. }
  17. /// <summary>
  18. /// Part of a path.
  19. /// This is either a sequence of adjacent triangles
  20. /// or a link.
  21. /// See: NodeLink2
  22. /// </summary>
  23. public struct PathPart {
  24. /// <summary>Index of the first node in this part</summary>
  25. public int startIndex;
  26. /// <summary>Index of the last node in this part</summary>
  27. public int endIndex;
  28. public Vector3 startPoint, endPoint;
  29. public bool isLink;
  30. }
  31. public static List<PathPart> SplitIntoParts (Path path) {
  32. var nodes = path.path;
  33. var result = ListPool<PathPart>.Claim();
  34. if (nodes == null || nodes.Count == 0) {
  35. return result;
  36. }
  37. // Loop through the path and split it into
  38. // parts joined by links
  39. for (int i = 0; i < nodes.Count; i++) {
  40. if (nodes[i] is TriangleMeshNode || nodes[i] is GridNodeBase) {
  41. var part = new PathPart();
  42. part.startIndex = i;
  43. uint currentGraphIndex = nodes[i].GraphIndex;
  44. // Loop up until we find a node in another graph
  45. // Ignore NodeLink3 nodes
  46. for (; i < nodes.Count; i++) {
  47. if (nodes[i].GraphIndex != currentGraphIndex && !(nodes[i] is NodeLink3Node)) {
  48. break;
  49. }
  50. }
  51. i--;
  52. part.endIndex = i;
  53. // If this is the first part in the path, use the exact start point
  54. // otherwise use the position of the node right before the start of this
  55. // part which is likely the end of the link to this part
  56. if (part.startIndex == 0) {
  57. part.startPoint = path.vectorPath[0];
  58. } else {
  59. part.startPoint = (Vector3)nodes[part.startIndex-1].position;
  60. }
  61. if (part.endIndex == nodes.Count-1) {
  62. part.endPoint = path.vectorPath[path.vectorPath.Count-1];
  63. } else {
  64. part.endPoint = (Vector3)nodes[part.endIndex+1].position;
  65. }
  66. result.Add(part);
  67. } else if (NodeLink2.GetNodeLink(nodes[i]) != null) {
  68. var part = new PathPart();
  69. part.startIndex = i;
  70. var currentGraphIndex = nodes[i].GraphIndex;
  71. for (i++; i < nodes.Count; i++) {
  72. if (nodes[i].GraphIndex != currentGraphIndex) {
  73. break;
  74. }
  75. }
  76. i--;
  77. if (i - part.startIndex == 0) {
  78. // Just ignore it, it might be the case that a NodeLink was the closest node
  79. continue;
  80. } else if (i - part.startIndex != 1) {
  81. throw new System.Exception("NodeLink2 link length greater than two (2) nodes. " + (i - part.startIndex + 1));
  82. }
  83. part.endIndex = i;
  84. part.isLink = true;
  85. part.startPoint = (Vector3)nodes[part.startIndex].position;
  86. part.endPoint = (Vector3)nodes[part.endIndex].position;
  87. result.Add(part);
  88. } else {
  89. throw new System.Exception("Unsupported node type or null node");
  90. }
  91. }
  92. return result;
  93. }
  94. public static FunnelPortals ConstructFunnelPortals (List<GraphNode> nodes, PathPart part) {
  95. if (nodes == null || nodes.Count == 0) {
  96. return new FunnelPortals { left = ListPool<Vector3>.Claim(0), right = ListPool<Vector3>.Claim(0) };
  97. }
  98. if (part.endIndex < part.startIndex || part.startIndex < 0 || part.endIndex > nodes.Count) throw new System.ArgumentOutOfRangeException();
  99. // Claim temporary lists and try to find lists with a high capacity
  100. var left = ListPool<Vector3>.Claim(nodes.Count+1);
  101. var right = ListPool<Vector3>.Claim(nodes.Count+1);
  102. // Add start point
  103. left.Add(part.startPoint);
  104. right.Add(part.startPoint);
  105. // Loop through all nodes in the path (except the last one)
  106. for (int i = part.startIndex; i < part.endIndex; i++) {
  107. // Get the portal between path[i] and path[i+1] and add it to the left and right lists
  108. bool portalWasAdded = nodes[i].GetPortal(nodes[i+1], left, right, false);
  109. if (!portalWasAdded) {
  110. // Fallback, just use the positions of the nodes
  111. left.Add((Vector3)nodes[i].position);
  112. right.Add((Vector3)nodes[i].position);
  113. left.Add((Vector3)nodes[i+1].position);
  114. right.Add((Vector3)nodes[i+1].position);
  115. }
  116. }
  117. // Add end point
  118. left.Add(part.endPoint);
  119. right.Add(part.endPoint);
  120. return new FunnelPortals { left = left, right = right };
  121. }
  122. public static void ShrinkPortals (FunnelPortals portals, float shrink) {
  123. if (shrink <= 0.00001f) return;
  124. for (int i = 0; i < portals.left.Count; i++) {
  125. var left = portals.left[i];
  126. var right = portals.right[i];
  127. var length = (left - right).magnitude;
  128. if (length > 0) {
  129. float s = Mathf.Min(shrink / length, 0.4f);
  130. portals.left[i] = Vector3.Lerp(left, right, s);
  131. portals.right[i] = Vector3.Lerp(left, right, 1 - s);
  132. }
  133. }
  134. }
  135. static bool UnwrapHelper (Vector3 portalStart, Vector3 portalEnd, Vector3 prevPoint, Vector3 nextPoint, ref Quaternion mRot, ref Vector3 mOffset) {
  136. // Skip the point if it was on the rotation axis
  137. if (VectorMath.IsColinear(portalStart, portalEnd, nextPoint)) {
  138. return false;
  139. }
  140. var axis = portalEnd - portalStart;
  141. var sqrMagn = axis.sqrMagnitude;
  142. prevPoint -= Vector3.Dot(prevPoint - portalStart, axis)/sqrMagn * axis;
  143. nextPoint -= Vector3.Dot(nextPoint - portalStart, axis)/sqrMagn * axis;
  144. var rot = Quaternion.FromToRotation(nextPoint - portalStart, portalStart - prevPoint);
  145. // The code below is equivalent to these matrix operations (but a lot faster)
  146. // This represents a rotation around a line in 3D space
  147. //mat = mat * Matrix4x4.TRS(portalStart, rot, Vector3.one) * Matrix4x4.TRS(-portalStart, Quaternion.identity, Vector3.one);
  148. mOffset += mRot * (portalStart - rot * portalStart);
  149. mRot *= rot;
  150. return true;
  151. }
  152. /// <summary>
  153. /// Unwraps the funnel portals from 3D space to 2D space.
  154. /// The result is stored in the left and right arrays which must be at least as large as the funnel.left and funnel.right lists.
  155. ///
  156. /// The input is a funnel like in the image below. It may be rotated and twisted.
  157. /// [Open online documentation to see images]
  158. /// The output will be a funnel in 2D space like in the image below. All twists and bends will have been straightened out.
  159. /// [Open online documentation to see images]
  160. ///
  161. /// See: <see cref="Calculate(FunnelPortals,bool,bool)"/>
  162. /// </summary>
  163. public static void Unwrap (FunnelPortals funnel, Vector2[] left, Vector2[] right) {
  164. int startingIndex = 1;
  165. var normal = Vector3.Cross(funnel.right[1] - funnel.left[0], funnel.left[1] - funnel.left[0]);
  166. // This handles the case when the starting point is colinear with the first portal.
  167. // Note that left.Length is only guaranteed to be at least as large as funnel.left.Count, it may be larger.
  168. while (normal.sqrMagnitude <= 0.00000001f && startingIndex + 1 < funnel.left.Count) {
  169. startingIndex++;
  170. normal = Vector3.Cross(funnel.right[startingIndex] - funnel.left[0], funnel.left[startingIndex] - funnel.left[0]);
  171. }
  172. left[0] = right[0] = Vector2.zero;
  173. var portalLeft = funnel.left[1];
  174. var portalRight = funnel.right[1];
  175. var prevPoint = funnel.left[0];
  176. // The code below is equivalent to this matrix (but a lot faster)
  177. // This represents a rotation around a line in 3D space
  178. // Matrix4x4 m = Matrix4x4.TRS(Vector3.zero, Quaternion.FromToRotation(normal, Vector3.forward), Vector3.one) * Matrix4x4.TRS(-funnel.right[0], Quaternion.identity, Vector3.one);
  179. Quaternion mRot = Quaternion.FromToRotation(normal, Vector3.forward);
  180. Vector3 mOffset = mRot * (-funnel.right[0]);
  181. for (int i = 1; i < funnel.left.Count; i++) {
  182. if (UnwrapHelper(portalLeft, portalRight, prevPoint, funnel.left[i], ref mRot, ref mOffset)) {
  183. prevPoint = portalLeft;
  184. portalLeft = funnel.left[i];
  185. }
  186. left[i] = mRot * funnel.left[i] + mOffset;
  187. if (UnwrapHelper(portalLeft, portalRight, prevPoint, funnel.right[i], ref mRot, ref mOffset)) {
  188. prevPoint = portalRight;
  189. portalRight = funnel.right[i];
  190. }
  191. right[i] = mRot * funnel.right[i] + mOffset;
  192. }
  193. }
  194. /// <summary>
  195. /// Try to fix degenerate or invalid funnels.
  196. /// Returns: The number of vertices at the start of both arrays that should be ignored or -1 if the algorithm failed.
  197. /// </summary>
  198. static int FixFunnel (Vector2[] left, Vector2[] right, int numPortals) {
  199. if (numPortals > left.Length || numPortals > right.Length) throw new System.ArgumentException("Arrays do not have as many elements as specified");
  200. if (numPortals < 3) {
  201. return -1;
  202. }
  203. // Remove duplicate vertices
  204. int startIndex = 0;
  205. while (left[startIndex + 1] == left[startIndex + 2] && right[startIndex + 1] == right[startIndex + 2]) {
  206. // Equivalent to RemoveAt(1) if they would have been lists
  207. left[startIndex + 1] = left[startIndex + 0];
  208. right[startIndex + 1] = right[startIndex + 0];
  209. startIndex++;
  210. if (numPortals - startIndex < 3) {
  211. return -1;
  212. }
  213. }
  214. return startIndex;
  215. }
  216. protected static Vector2 ToXZ (Vector3 p) {
  217. return new Vector2(p.x, p.z);
  218. }
  219. protected static Vector3 FromXZ (Vector2 p) {
  220. return new Vector3(p.x, 0, p.y);
  221. }
  222. /// <summary>True if b is to the right of or on the line from (0,0) to a</summary>
  223. protected static bool RightOrColinear (Vector2 a, Vector2 b) {
  224. return (a.x*b.y - b.x*a.y) <= 0;
  225. }
  226. /// <summary>True if b is to the left of or on the line from (0,0) to a</summary>
  227. protected static bool LeftOrColinear (Vector2 a, Vector2 b) {
  228. return (a.x*b.y - b.x*a.y) >= 0;
  229. }
  230. /// <summary>
  231. /// Calculate the shortest path through the funnel.
  232. ///
  233. /// If the unwrap option is disabled the funnel will simply be projected onto the XZ plane.
  234. /// If the unwrap option is enabled then the funnel may be oriented arbitrarily and may have twists and bends.
  235. /// This makes it possible to support the funnel algorithm in XY space as well as in more complicated cases, such
  236. /// as on curved worlds.
  237. /// [Open online documentation to see images]
  238. ///
  239. /// [Open online documentation to see images]
  240. ///
  241. /// See: Unwrap
  242. /// </summary>
  243. /// <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>
  244. /// <param name="unwrap">Determines if twists and bends should be straightened out before running the funnel algorithm.</param>
  245. /// <param name="splitAtEveryPortal">If true, then a vertex will be inserted every time the path crosses a portal
  246. /// instead of only at the corners of the path. The result will have exactly one vertex per portal if this is enabled.
  247. /// This may introduce vertices with the same position in the output (esp. in corners where many portals meet).</param>
  248. public static List<Vector3> Calculate (FunnelPortals funnel, bool unwrap, bool splitAtEveryPortal) {
  249. if (funnel.left.Count != funnel.right.Count) throw new System.ArgumentException("funnel.left.Count != funnel.right.Count");
  250. // Get arrays at least as large as the number of portals
  251. var leftArr = ArrayPool<Vector2>.Claim(funnel.left.Count);
  252. var rightArr = ArrayPool<Vector2>.Claim(funnel.left.Count);
  253. if (unwrap) {
  254. Unwrap(funnel, leftArr, rightArr);
  255. } else {
  256. // Copy to arrays
  257. for (int i = 0; i < funnel.left.Count; i++) {
  258. leftArr[i] = ToXZ(funnel.left[i]);
  259. rightArr[i] = ToXZ(funnel.right[i]);
  260. }
  261. }
  262. int startIndex = FixFunnel(leftArr, rightArr, funnel.left.Count);
  263. var intermediateResult = ListPool<int>.Claim();
  264. if (startIndex == -1) {
  265. // If funnel algorithm failed, fall back to a simple line
  266. intermediateResult.Add(0);
  267. intermediateResult.Add(funnel.left.Count - 1);
  268. } else {
  269. bool lastCorner;
  270. Calculate(leftArr, rightArr, funnel.left.Count, startIndex, intermediateResult, int.MaxValue, out lastCorner);
  271. }
  272. // Get list for the final result
  273. var result = ListPool<Vector3>.Claim(intermediateResult.Count);
  274. Vector2 prev2D = leftArr[0];
  275. var prevIdx = 0;
  276. for (int i = 0; i < intermediateResult.Count; i++) {
  277. var idx = intermediateResult[i];
  278. if (splitAtEveryPortal) {
  279. // Check intersections with every portal segment
  280. var next2D = idx >= 0 ? leftArr[idx] : rightArr[-idx];
  281. for (int j = prevIdx + 1; j < System.Math.Abs(idx); j++) {
  282. var factor = VectorMath.LineIntersectionFactorXZ(FromXZ(leftArr[j]), FromXZ(rightArr[j]), FromXZ(prev2D), FromXZ(next2D));
  283. result.Add(Vector3.Lerp(funnel.left[j], funnel.right[j], factor));
  284. }
  285. prevIdx = Mathf.Abs(idx);
  286. prev2D = next2D;
  287. }
  288. if (idx >= 0) {
  289. result.Add(funnel.left[idx]);
  290. } else {
  291. result.Add(funnel.right[-idx]);
  292. }
  293. }
  294. // Release lists back to the pool
  295. ListPool<int>.Release(ref intermediateResult);
  296. ArrayPool<Vector2>.Release(ref leftArr);
  297. ArrayPool<Vector2>.Release(ref rightArr);
  298. return result;
  299. }
  300. /// <summary>
  301. /// Funnel algorithm.
  302. /// funnelPath will be filled with the result.
  303. /// The result is the indices of the vertices that were picked, a non-negative value refers to the corresponding index in the
  304. /// left array, a negative value refers to the corresponding index in the right array.
  305. /// So e.g 5 corresponds to left[5] and -2 corresponds to right[2]
  306. ///
  307. /// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
  308. /// </summary>
  309. static void Calculate (Vector2[] left, Vector2[] right, int numPortals, int startIndex, List<int> funnelPath, int maxCorners, out bool lastCorner) {
  310. if (left.Length != right.Length) throw new System.ArgumentException();
  311. lastCorner = false;
  312. int apexIndex = startIndex + 0;
  313. int rightIndex = startIndex + 1;
  314. int leftIndex = startIndex + 1;
  315. Vector2 portalApex = left[apexIndex];
  316. Vector2 portalLeft = left[leftIndex];
  317. Vector2 portalRight = right[rightIndex];
  318. funnelPath.Add(apexIndex);
  319. for (int i = startIndex + 2; i < numPortals; i++) {
  320. if (funnelPath.Count >= maxCorners) {
  321. return;
  322. }
  323. if (funnelPath.Count > 2000) {
  324. Debug.LogWarning("Avoiding infinite loop. Remove this check if you have this long paths.");
  325. break;
  326. }
  327. Vector2 pLeft = left[i];
  328. Vector2 pRight = right[i];
  329. if (LeftOrColinear(portalRight - portalApex, pRight - portalApex)) {
  330. if (portalApex == portalRight || RightOrColinear(portalLeft - portalApex, pRight - portalApex)) {
  331. portalRight = pRight;
  332. rightIndex = i;
  333. } else {
  334. portalApex = portalRight = portalLeft;
  335. i = apexIndex = rightIndex = leftIndex;
  336. funnelPath.Add(apexIndex);
  337. continue;
  338. }
  339. }
  340. if (RightOrColinear(portalLeft - portalApex, pLeft - portalApex)) {
  341. if (portalApex == portalLeft || LeftOrColinear(portalRight - portalApex, pLeft - portalApex)) {
  342. portalLeft = pLeft;
  343. leftIndex = i;
  344. } else {
  345. portalApex = portalLeft = portalRight;
  346. i = apexIndex = leftIndex = rightIndex;
  347. // Negative value because we are referring
  348. // to the right side
  349. funnelPath.Add(-apexIndex);
  350. continue;
  351. }
  352. }
  353. }
  354. lastCorner = true;
  355. funnelPath.Add(numPortals-1);
  356. }
  357. }
  358. }