Funnel.cs 14 KB

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