ConfinerOven.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Cinemachine.Utility;
  5. using UnityEngine;
  6. namespace Cinemachine
  7. {
  8. using IntPoint = ClipperLib.IntPoint;
  9. using Clipper = ClipperLib.Clipper;
  10. using ClipperOffset = ClipperLib.ClipperOffset;
  11. using ClipperBase = ClipperLib.ClipperBase;
  12. using IntRect = ClipperLib.IntRect;
  13. using JoinType = ClipperLib.JoinType;
  14. using EndType = ClipperLib.EndType;
  15. using PolyType = ClipperLib.PolyType;
  16. using PolyFillType = ClipperLib.PolyFillType;
  17. using ClipType = ClipperLib.ClipType;
  18. /// <summary>
  19. /// Responsible for baking confiners via BakeConfiner function.
  20. /// </summary>
  21. class ConfinerOven
  22. {
  23. public class BakedSolution
  24. {
  25. float m_FrustumSizeIntSpace;
  26. readonly AspectStretcher m_AspectStretcher;
  27. readonly bool m_HasBones;
  28. readonly double m_SqrPolygonDiagonal;
  29. List<List<IntPoint>> m_OriginalPolygon;
  30. List<List<IntPoint>> m_Solution;
  31. const double k_ClipperEpsilon = 0.01f * k_FloatToIntScaler;
  32. public BakedSolution(
  33. float aspectRatio, float frustumHeight, bool hasBones, Rect polygonBounds,
  34. List<List<IntPoint>> originalPolygon, List<List<IntPoint>> solution)
  35. {
  36. m_AspectStretcher = new AspectStretcher(aspectRatio, polygonBounds.center.x);
  37. m_FrustumSizeIntSpace = frustumHeight * k_FloatToIntScaler;
  38. m_HasBones = hasBones;
  39. m_OriginalPolygon = originalPolygon;
  40. m_Solution = solution;
  41. float polygonSizeX = polygonBounds.width / aspectRatio * k_FloatToIntScaler;
  42. float polygonSizeY = polygonBounds.height * k_FloatToIntScaler;
  43. m_SqrPolygonDiagonal = polygonSizeX * polygonSizeX + polygonSizeY * polygonSizeY;
  44. }
  45. public bool IsValid() => m_Solution != null;
  46. public Vector2 ConfinePoint(in Vector2 pointToConfine)
  47. {
  48. if (m_Solution.Count <= 0) return pointToConfine; // empty confiner -> no need to confine
  49. Vector2 pInConfinerSpace = m_AspectStretcher.Stretch(pointToConfine);
  50. IntPoint p =
  51. new IntPoint(pInConfinerSpace.x * k_FloatToIntScaler, pInConfinerSpace.y * k_FloatToIntScaler);
  52. for (int i = 0; i < m_Solution.Count; ++i)
  53. {
  54. if (Clipper.PointInPolygon(p, m_Solution[i]) != 0) // 0: outside, +1: inside , -1: point on poly boundary
  55. {
  56. return pointToConfine; // inside, no confinement needed
  57. }
  58. }
  59. // If the poly has bones and if the position to confine is not outside of the original
  60. // bounding shape, then it is possible that the bone in a neighbouring section
  61. // is closer than the bone in the correct section of the polygon, if the current section
  62. // is very large and the neighbouring section is small. In that case, we'll need to
  63. // add an extra check when calculating the nearest point.
  64. bool checkIntersectOriginal = m_HasBones && IsInsideOriginal(p);
  65. // Confine point
  66. IntPoint closest = p;
  67. double minDistance = double.MaxValue;
  68. for (int i = 0; i < m_Solution.Count; ++i)
  69. {
  70. int numPoints = m_Solution[i].Count;
  71. for (int j = 0; j < numPoints; ++j)
  72. {
  73. IntPoint l1 = m_Solution[i][j];
  74. IntPoint l2 = m_Solution[i][(j + 1) % numPoints];
  75. IntPoint c = IntPointLerp(l1, l2, ClosestPointOnSegment(p, l1, l2));
  76. double diffX = Mathf.Abs(p.X - c.X);
  77. double diffY = Mathf.Abs(p.Y - c.Y);
  78. double distance = diffX * diffX + diffY * diffY;
  79. // penalty for points from which the target is not visible, preferring visibility over proximity
  80. if (diffX > m_FrustumSizeIntSpace || diffY > m_FrustumSizeIntSpace)
  81. {
  82. distance += m_SqrPolygonDiagonal; // penalty is the biggest distance between any two points
  83. }
  84. if (distance < minDistance && (!checkIntersectOriginal || !DoesIntersectOriginal(p, c)))
  85. {
  86. minDistance = distance;
  87. closest = c;
  88. }
  89. }
  90. }
  91. var result = new Vector2(closest.X * k_IntToFloatScaler, closest.Y * k_IntToFloatScaler);
  92. return m_AspectStretcher.Unstretch(result);
  93. // local functions
  94. IntPoint IntPointLerp(IntPoint a, IntPoint b, float lerp)
  95. {
  96. return new IntPoint
  97. {
  98. X = Mathf.RoundToInt(a.X + (b.X - a.X) * lerp),
  99. Y = Mathf.RoundToInt(a.Y + (b.Y - a.Y) * lerp),
  100. };
  101. }
  102. bool IsInsideOriginal(IntPoint point) =>
  103. m_OriginalPolygon.Any(t => Clipper.PointInPolygon(point, t) != 0);
  104. float ClosestPointOnSegment(IntPoint point, IntPoint s0, IntPoint s1)
  105. {
  106. double sX = s1.X - s0.X;
  107. double sY = s1.Y - s0.Y;
  108. var len2 = sX * sX + sY * sY;
  109. if (len2 < k_ClipperEpsilon)
  110. return 0; // degenerate segment
  111. double s0pX = point.X - s0.X;
  112. double s0pY = point.Y - s0.Y;
  113. var dot = s0pX * sX + s0pY * sY;
  114. return Mathf.Clamp01((float) (dot / len2));
  115. }
  116. bool DoesIntersectOriginal(IntPoint l1, IntPoint l2)
  117. {
  118. foreach (var original in m_OriginalPolygon)
  119. {
  120. var numPoints = original.Count;
  121. for (var i = 0; i < numPoints; ++i)
  122. {
  123. if (FindIntersection(l1, l2, original[i], original[(i + 1) % numPoints]) == 2)
  124. {
  125. return true;
  126. }
  127. }
  128. }
  129. return false;
  130. }
  131. }
  132. #if UNITY_EDITOR
  133. // Used by inspector to draw the baked path
  134. List<List<Vector2>> m_Vector2Path;
  135. public List<List<Vector2>> GetBakedPath()
  136. {
  137. // Convert to client space
  138. var numPaths = m_Solution.Count;
  139. if (m_Vector2Path == null)
  140. {
  141. m_Vector2Path = new List<List<Vector2>>(numPaths);
  142. for (int i = 0; i < numPaths; ++i)
  143. {
  144. var srcPoly = m_Solution[i];
  145. int numPoints = srcPoly.Count;
  146. var pathSegment = new List<Vector2>(numPoints);
  147. for (int j = 0; j < numPoints; j++)
  148. {
  149. // Restore the original aspect ratio
  150. pathSegment.Add(m_AspectStretcher.Unstretch(
  151. new Vector2(srcPoly[j].X, srcPoly[j].Y) * k_IntToFloatScaler));
  152. }
  153. m_Vector2Path.Add(pathSegment);
  154. }
  155. }
  156. return m_Vector2Path;
  157. }
  158. #endif
  159. static int FindIntersection(in IntPoint p1, in IntPoint p2, in IntPoint p3, in IntPoint p4)
  160. {
  161. // Get the segments' parameters.
  162. double dx12 = p2.X - p1.X;
  163. double dy12 = p2.Y - p1.Y;
  164. double dx34 = p4.X - p3.X;
  165. double dy34 = p4.Y - p3.Y;
  166. // Solve for t1 and t2
  167. double denominator = dy12 * dx34 - dx12 * dy34;
  168. double t1 =
  169. ((p1.X - p3.X) * dy34 + (p3.Y - p1.Y) * dx34)
  170. / denominator;
  171. if (double.IsInfinity(t1) || double.IsNaN(t1))
  172. {
  173. // The lines are parallel (or close enough to it).
  174. if (IntPointDiffSqrMagnitude(p1, p3) < k_ClipperEpsilon ||
  175. IntPointDiffSqrMagnitude(p1, p4) < k_ClipperEpsilon ||
  176. IntPointDiffSqrMagnitude(p2, p3) < k_ClipperEpsilon ||
  177. IntPointDiffSqrMagnitude(p2, p4) < k_ClipperEpsilon)
  178. {
  179. return 2; // they are the same line, or very close parallels
  180. }
  181. return 0; // no intersection
  182. }
  183. double t2 = ((p3.X - p1.X) * dy12 + (p1.Y - p3.Y) * dx12) / -denominator;
  184. return (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 < 1) ? 2 : 1; // 2 = segments intersect, 1 = lines intersect
  185. // local function
  186. double IntPointDiffSqrMagnitude(IntPoint point1, IntPoint point2)
  187. {
  188. double x = point1.X - point2.X;
  189. double y = point1.Y - point2.Y;
  190. return x * x + y * y;
  191. }
  192. }
  193. }
  194. readonly struct AspectStretcher
  195. {
  196. public float Aspect { get; }
  197. readonly float m_InverseAspect;
  198. readonly float m_CenterX;
  199. public AspectStretcher(float aspect, float centerX)
  200. {
  201. Aspect = aspect;
  202. m_InverseAspect = 1 / Aspect;
  203. m_CenterX = centerX;
  204. }
  205. public Vector2 Stretch(Vector2 p) => new Vector2((p.x - m_CenterX) * m_InverseAspect + m_CenterX, p.y);
  206. public Vector2 Unstretch(Vector2 p) => new Vector2((p.x - m_CenterX) * Aspect + m_CenterX, p.y);
  207. }
  208. float m_MinFrustumHeightWithBones;
  209. List<List<IntPoint>> m_OriginalPolygon;
  210. IntPoint m_MidPoint;
  211. List<List<IntPoint>> m_Skeleton = new List<List<IntPoint>>();
  212. const long k_FloatToIntScaler = 100000;
  213. const float k_IntToFloatScaler = 1.0f / k_FloatToIntScaler;
  214. const float k_MinStepSize = 0.005f;
  215. Rect m_PolygonRect;
  216. AspectStretcher m_AspectStretcher = new AspectStretcher(1, 0);
  217. float m_MaxComputationTimeForFullSkeletonBakeInSeconds = 5f;
  218. public ConfinerOven(in List<List<Vector2>> inputPath, in float aspectRatio, float maxFrustumHeight)
  219. {
  220. Initialize(inputPath, aspectRatio, maxFrustumHeight);
  221. }
  222. /// <summary>
  223. /// Converts and returns a prebaked ConfinerState for the input frustumHeight.
  224. /// </summary>
  225. public BakedSolution GetBakedSolution(float frustumHeight)
  226. {
  227. // If the user has set a max frustum height, respect it
  228. frustumHeight = m_Cache.userSetMaxFrustumHeight <= 0
  229. ? frustumHeight
  230. : Mathf.Min(m_Cache.userSetMaxFrustumHeight, frustumHeight);
  231. // Special case: we are shrank to the mid point of the original input confiner area.
  232. if (State == BakingState.BAKED && frustumHeight > m_Cache.theoriticalMaxFrustumHeight)
  233. {
  234. return new BakedSolution(
  235. m_AspectStretcher.Aspect, frustumHeight, false,
  236. m_PolygonRect, m_OriginalPolygon,
  237. new List<List<IntPoint>>{new List<IntPoint> { m_MidPoint }});
  238. }
  239. // Inflate with clipper to frustumHeight
  240. var offsetter = new ClipperOffset();
  241. offsetter.AddPaths(m_OriginalPolygon, JoinType.jtMiter, EndType.etClosedPolygon);
  242. var solution = new List<List<IntPoint>>();
  243. offsetter.Execute(ref solution, -1f * frustumHeight * k_FloatToIntScaler);
  244. // Add in the skeleton
  245. var bakedSolution = new List<List<IntPoint>>();
  246. if (State == BakingState.BAKING || m_Skeleton.Count == 0)
  247. {
  248. bakedSolution = solution;
  249. }
  250. else
  251. {
  252. var c = new Clipper();
  253. c.AddPaths(solution, PolyType.ptSubject, true);
  254. c.AddPaths(m_Skeleton, PolyType.ptClip, true);
  255. c.Execute(ClipType.ctUnion, bakedSolution, PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd);
  256. }
  257. return new BakedSolution(
  258. m_AspectStretcher.Aspect, frustumHeight,
  259. m_MinFrustumHeightWithBones < frustumHeight,
  260. m_PolygonRect, m_OriginalPolygon, bakedSolution);
  261. }
  262. struct PolygonSolution
  263. {
  264. public List<List<IntPoint>> polygons;
  265. public float frustumHeight;
  266. public bool StateChanged(in List<List<IntPoint>> paths)
  267. {
  268. if (paths.Count != polygons.Count)
  269. return true;
  270. for (var i = 0; i < paths.Count; ++i)
  271. {
  272. if (paths[i].Count != polygons[i].Count)
  273. return true;
  274. }
  275. return false;
  276. }
  277. public bool IsNull => polygons == null;
  278. }
  279. public enum BakingState
  280. {
  281. BAKING, BAKED, TIMEOUT
  282. }
  283. public BakingState State { get; private set; }
  284. public float bakeProgress;
  285. struct BakingStateCache
  286. {
  287. public ClipperOffset offsetter;
  288. public List<PolygonSolution> solutions;
  289. public PolygonSolution rightCandidate;
  290. public PolygonSolution leftCandidate;
  291. public List<List<IntPoint>> maxCandidate;
  292. public float stepSize;
  293. public float maxFrustumHeight;
  294. public float userSetMaxFrustumHeight;
  295. public float theoriticalMaxFrustumHeight;
  296. public float currentFrustumHeight;
  297. public float bakeTime;
  298. }
  299. BakingStateCache m_Cache;
  300. void Initialize(in List<List<Vector2>> inputPath, in float aspectRatio, float maxFrustumHeight)
  301. {
  302. m_Skeleton.Clear();
  303. m_Cache.userSetMaxFrustumHeight = maxFrustumHeight;
  304. m_MinFrustumHeightWithBones = float.MaxValue;
  305. // calculate mid point and use it as the most shrank down version
  306. m_PolygonRect = GetPolygonBoundingBox(inputPath);
  307. m_AspectStretcher = new AspectStretcher(aspectRatio, m_PolygonRect.center.x);
  308. // Don't compute further than what is the theoretical max
  309. m_Cache.theoriticalMaxFrustumHeight = Mathf.Max(m_PolygonRect.width / aspectRatio, m_PolygonRect.height) / 2f;
  310. // Initialize clipper
  311. m_OriginalPolygon = new List<List<IntPoint>>(inputPath.Count);
  312. for (var i = 0; i < inputPath.Count; ++i)
  313. {
  314. var srcPath = inputPath[i];
  315. var numPoints = srcPath.Count;
  316. var path = new List<IntPoint>(numPoints);
  317. for (var j = 0; j < numPoints; ++j)
  318. {
  319. // Neutralize the aspect ratio
  320. var p = m_AspectStretcher.Stretch(srcPath[j]);
  321. path.Add(new IntPoint(p.x * k_FloatToIntScaler, p.y * k_FloatToIntScaler));
  322. }
  323. m_OriginalPolygon.Add(path);
  324. }
  325. m_MidPoint = MidPointOfIntRect(ClipperBase.GetBounds(m_OriginalPolygon));
  326. // Skip the expensive skeleton calculation if it's not wanted (oversized window off)
  327. if (m_Cache.userSetMaxFrustumHeight < 0)
  328. {
  329. State = BakingState.BAKED; // if we don't need skeleton, then we don't need to bake
  330. return;
  331. }
  332. // exact comparison to 0 is intentional!
  333. m_Cache.maxFrustumHeight = m_Cache.userSetMaxFrustumHeight;
  334. if (m_Cache.maxFrustumHeight == 0 || m_Cache.maxFrustumHeight > m_Cache.theoriticalMaxFrustumHeight)
  335. {
  336. m_Cache.maxFrustumHeight = m_Cache.theoriticalMaxFrustumHeight;
  337. }
  338. m_Cache.stepSize = m_Cache.maxFrustumHeight;
  339. // Binary search for state changes so we can compute the skeleton
  340. m_Cache.offsetter = new ClipperOffset();
  341. m_Cache.offsetter.AddPaths(m_OriginalPolygon, JoinType.jtMiter, EndType.etClosedPolygon);
  342. var solution = new List<List<IntPoint>>();
  343. m_Cache.offsetter.Execute(ref solution, 0);
  344. m_Cache.solutions = new List<PolygonSolution>();
  345. m_Cache.solutions.Add(new PolygonSolution
  346. {
  347. polygons = solution,
  348. frustumHeight = 0,
  349. });
  350. m_Cache.rightCandidate = new PolygonSolution();
  351. m_Cache.leftCandidate = new PolygonSolution
  352. {
  353. polygons = solution,
  354. frustumHeight = 0,
  355. };
  356. m_Cache.currentFrustumHeight = 0;
  357. m_Cache.maxCandidate = new List<List<IntPoint>>();
  358. m_Cache.offsetter.Execute(ref m_Cache.maxCandidate, -1f * m_Cache.theoriticalMaxFrustumHeight * k_FloatToIntScaler);
  359. m_Cache.bakeTime = 0;
  360. State = BakingState.BAKING;
  361. bakeProgress = 0;
  362. // local functions
  363. Rect GetPolygonBoundingBox(in List<List<Vector2>> polygons)
  364. {
  365. float minX = float.PositiveInfinity, maxX = float.NegativeInfinity;
  366. float minY = float.PositiveInfinity, maxY = float.NegativeInfinity;
  367. for (var i = 0; i < polygons.Count; ++i)
  368. {
  369. for (var j = 0; j < polygons[i].Count; ++j)
  370. {
  371. var p = polygons[i][j];
  372. minX = Mathf.Min(minX, p.x);
  373. maxX = Mathf.Max(maxX, p.x);
  374. minY = Mathf.Min(minY, p.y);
  375. maxY = Mathf.Max(maxY, p.y);
  376. }
  377. }
  378. return new Rect(minX, minY, Mathf.Max(0, maxX - minX), Mathf.Max(0, maxY - minY));
  379. }
  380. IntPoint MidPointOfIntRect(IntRect bounds) =>
  381. new IntPoint((bounds.left + bounds.right) / 2, (bounds.top + bounds.bottom) / 2);
  382. }
  383. /// <summary>
  384. /// Creates shrinkable polygons from input parameters.
  385. /// The algorithm is divide and conquer. It iteratively shrinks down the input
  386. /// polygon towards its shrink directions. If the polygon intersects with itself,
  387. /// then we divide the polygon into two polygons at the intersection point, and
  388. /// continue the algorithm on these two polygons separately. We need to keep track of
  389. /// the connectivity information between sub-polygons.
  390. /// </summary>
  391. public void BakeConfiner(float maxComputationTimePerFrameInSeconds)
  392. {
  393. if (State != BakingState.BAKING)
  394. return;
  395. var startTime = Time.realtimeSinceStartup;
  396. while (m_Cache.solutions.Count < 1000)
  397. {
  398. var numPaths = m_Cache.leftCandidate.polygons.Count;
  399. var candidate = new List<List<IntPoint>>(numPaths);
  400. m_Cache.stepSize = Mathf.Min(m_Cache.stepSize,
  401. m_Cache.maxFrustumHeight - m_Cache.leftCandidate.frustumHeight);
  402. #if false
  403. Debug.Log($"States = {solutions.Count}, "
  404. + $"Frustum height = {currentFrustumHeight}, stepSize = {stepSize}");
  405. #endif
  406. m_Cache.currentFrustumHeight =
  407. m_Cache.leftCandidate.frustumHeight + m_Cache.stepSize;
  408. if (Math.Abs(m_Cache.currentFrustumHeight - m_Cache.maxFrustumHeight) <
  409. UnityVectorExtensions.Epsilon)
  410. {
  411. candidate = m_Cache.maxCandidate;
  412. }
  413. else
  414. {
  415. m_Cache.offsetter.Execute(
  416. ref candidate, -1f * m_Cache.currentFrustumHeight * k_FloatToIntScaler);
  417. }
  418. if (m_Cache.leftCandidate.StateChanged(in candidate))
  419. {
  420. m_Cache.rightCandidate = new PolygonSolution
  421. {
  422. polygons = candidate,
  423. frustumHeight = m_Cache.currentFrustumHeight,
  424. };
  425. m_Cache.stepSize = Mathf.Max(m_Cache.stepSize / 2f, k_MinStepSize);
  426. }
  427. else
  428. {
  429. m_Cache.leftCandidate = new PolygonSolution
  430. {
  431. polygons = candidate,
  432. frustumHeight = m_Cache.currentFrustumHeight,
  433. };
  434. // if we have not found right yet, then we don't need to decrease stepsize
  435. if (!m_Cache.rightCandidate.IsNull)
  436. {
  437. m_Cache.stepSize = Mathf.Max(m_Cache.stepSize / 2f, k_MinStepSize);
  438. }
  439. }
  440. // if we have a right candidate, and left and right are sufficiently close,
  441. // then we have located a state change point
  442. if (!m_Cache.rightCandidate.IsNull && m_Cache.stepSize <= k_MinStepSize)
  443. {
  444. // Add both states: one before the state change and one after
  445. m_Cache.solutions.Add(m_Cache.leftCandidate);
  446. m_Cache.solutions.Add(m_Cache.rightCandidate);
  447. m_Cache.leftCandidate = m_Cache.rightCandidate;
  448. m_Cache.rightCandidate = new PolygonSolution();
  449. // Back to max step
  450. m_Cache.stepSize = m_Cache.maxFrustumHeight;
  451. }
  452. else if (m_Cache.rightCandidate.IsNull ||
  453. m_Cache.leftCandidate.frustumHeight >= m_Cache.maxFrustumHeight)
  454. {
  455. m_Cache.solutions.Add(m_Cache.leftCandidate);
  456. break; // stop searching, because we are at the bound
  457. }
  458. // Pause after max time per iteration reached
  459. var elapsedTime = Time.realtimeSinceStartup - startTime;
  460. if (elapsedTime > maxComputationTimePerFrameInSeconds)
  461. {
  462. m_Cache.bakeTime += elapsedTime;
  463. if (m_Cache.bakeTime > m_MaxComputationTimeForFullSkeletonBakeInSeconds)
  464. {
  465. State = BakingState.TIMEOUT;
  466. }
  467. bakeProgress = m_Cache.leftCandidate.frustumHeight / m_Cache.maxFrustumHeight;
  468. return;
  469. }
  470. }
  471. ComputeSkeleton(in m_Cache.solutions);
  472. // Remove useless/empty results
  473. for (var i = m_Cache.solutions.Count - 1; i >= 0; --i)
  474. {
  475. if (m_Cache.solutions[i].polygons.Count == 0)
  476. {
  477. m_Cache.solutions.RemoveAt(i);
  478. }
  479. }
  480. bakeProgress = 1;
  481. State = BakingState.BAKED;
  482. void ComputeSkeleton(in List<PolygonSolution> solutions)
  483. {
  484. // At each state change point, collect geometry that gets lost over the transition
  485. var clipper = new Clipper();
  486. var offsetter = new ClipperOffset();
  487. for (int i = 1; i < solutions.Count - 1; i += 2)
  488. {
  489. var prev = solutions[i];
  490. var next = solutions[i+1];
  491. const int padding = 5; // to counteract precision problems - inflates small regions
  492. double step = padding * k_FloatToIntScaler * (next.frustumHeight - prev.frustumHeight);
  493. // Grow the larger polygon to inflate marginal regions
  494. var expandedPrev = new List<List<IntPoint>>();
  495. offsetter.Clear();
  496. offsetter.AddPaths(prev.polygons, JoinType.jtMiter, EndType.etClosedPolygon);
  497. offsetter.Execute(ref expandedPrev, step);
  498. // Grow the smaller polygon to be a bit bigger than the expanded larger one
  499. var expandedNext = new List<List<IntPoint>>();
  500. offsetter.Clear();
  501. offsetter.AddPaths(next.polygons, JoinType.jtMiter, EndType.etClosedPolygon);
  502. offsetter.Execute(ref expandedNext, step * 2);
  503. // Compute the difference - this is the lost geometry
  504. var solution = new List<List<IntPoint>>();
  505. clipper.Clear();
  506. clipper.AddPaths(expandedPrev, PolyType.ptSubject, true);
  507. clipper.AddPaths(expandedNext, PolyType.ptClip, true);
  508. clipper.Execute(ClipType.ctDifference, solution, PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd);
  509. // Add that lost geometry to the skeleton
  510. if (solution.Count > 0 && solution[0].Count > 0)
  511. {
  512. m_Skeleton.AddRange(solution);
  513. // Exact comparison is intentional
  514. if (m_MinFrustumHeightWithBones == float.MaxValue)
  515. m_MinFrustumHeightWithBones = next.frustumHeight;
  516. }
  517. }
  518. }
  519. }
  520. }
  521. }