| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Cinemachine.Utility;
- using UnityEngine;
- namespace Cinemachine
- {
- using IntPoint = ClipperLib.IntPoint;
- using Clipper = ClipperLib.Clipper;
- using ClipperOffset = ClipperLib.ClipperOffset;
- using ClipperBase = ClipperLib.ClipperBase;
- using IntRect = ClipperLib.IntRect;
- using JoinType = ClipperLib.JoinType;
- using EndType = ClipperLib.EndType;
- using PolyType = ClipperLib.PolyType;
- using PolyFillType = ClipperLib.PolyFillType;
- using ClipType = ClipperLib.ClipType;
- /// <summary>
- /// Responsible for baking confiners via BakeConfiner function.
- /// </summary>
- class ConfinerOven
- {
- public class BakedSolution
- {
- float m_FrustumSizeIntSpace;
- readonly AspectStretcher m_AspectStretcher;
- readonly bool m_HasBones;
- readonly double m_SqrPolygonDiagonal;
- List<List<IntPoint>> m_OriginalPolygon;
- List<List<IntPoint>> m_Solution;
- const double k_ClipperEpsilon = 0.01f * k_FloatToIntScaler;
- public BakedSolution(
- float aspectRatio, float frustumHeight, bool hasBones, Rect polygonBounds,
- List<List<IntPoint>> originalPolygon, List<List<IntPoint>> solution)
- {
- m_AspectStretcher = new AspectStretcher(aspectRatio, polygonBounds.center.x);
- m_FrustumSizeIntSpace = frustumHeight * k_FloatToIntScaler;
- m_HasBones = hasBones;
- m_OriginalPolygon = originalPolygon;
- m_Solution = solution;
- float polygonSizeX = polygonBounds.width / aspectRatio * k_FloatToIntScaler;
- float polygonSizeY = polygonBounds.height * k_FloatToIntScaler;
- m_SqrPolygonDiagonal = polygonSizeX * polygonSizeX + polygonSizeY * polygonSizeY;
- }
- public bool IsValid() => m_Solution != null;
- public Vector2 ConfinePoint(in Vector2 pointToConfine)
- {
- if (m_Solution.Count <= 0) return pointToConfine; // empty confiner -> no need to confine
- Vector2 pInConfinerSpace = m_AspectStretcher.Stretch(pointToConfine);
- IntPoint p =
- new IntPoint(pInConfinerSpace.x * k_FloatToIntScaler, pInConfinerSpace.y * k_FloatToIntScaler);
- for (int i = 0; i < m_Solution.Count; ++i)
- {
- if (Clipper.PointInPolygon(p, m_Solution[i]) != 0) // 0: outside, +1: inside , -1: point on poly boundary
- {
- return pointToConfine; // inside, no confinement needed
- }
- }
- // If the poly has bones and if the position to confine is not outside of the original
- // bounding shape, then it is possible that the bone in a neighbouring section
- // is closer than the bone in the correct section of the polygon, if the current section
- // is very large and the neighbouring section is small. In that case, we'll need to
- // add an extra check when calculating the nearest point.
- bool checkIntersectOriginal = m_HasBones && IsInsideOriginal(p);
- // Confine point
- IntPoint closest = p;
- double minDistance = double.MaxValue;
- for (int i = 0; i < m_Solution.Count; ++i)
- {
- int numPoints = m_Solution[i].Count;
- for (int j = 0; j < numPoints; ++j)
- {
- IntPoint l1 = m_Solution[i][j];
- IntPoint l2 = m_Solution[i][(j + 1) % numPoints];
- IntPoint c = IntPointLerp(l1, l2, ClosestPointOnSegment(p, l1, l2));
- double diffX = Mathf.Abs(p.X - c.X);
- double diffY = Mathf.Abs(p.Y - c.Y);
- double distance = diffX * diffX + diffY * diffY;
- // penalty for points from which the target is not visible, preferring visibility over proximity
- if (diffX > m_FrustumSizeIntSpace || diffY > m_FrustumSizeIntSpace)
- {
- distance += m_SqrPolygonDiagonal; // penalty is the biggest distance between any two points
- }
- if (distance < minDistance && (!checkIntersectOriginal || !DoesIntersectOriginal(p, c)))
- {
- minDistance = distance;
- closest = c;
- }
- }
- }
- var result = new Vector2(closest.X * k_IntToFloatScaler, closest.Y * k_IntToFloatScaler);
- return m_AspectStretcher.Unstretch(result);
-
- // local functions
- IntPoint IntPointLerp(IntPoint a, IntPoint b, float lerp)
- {
- return new IntPoint
- {
- X = Mathf.RoundToInt(a.X + (b.X - a.X) * lerp),
- Y = Mathf.RoundToInt(a.Y + (b.Y - a.Y) * lerp),
- };
- }
-
- bool IsInsideOriginal(IntPoint point) =>
- m_OriginalPolygon.Any(t => Clipper.PointInPolygon(point, t) != 0);
- float ClosestPointOnSegment(IntPoint point, IntPoint s0, IntPoint s1)
- {
- double sX = s1.X - s0.X;
- double sY = s1.Y - s0.Y;
- var len2 = sX * sX + sY * sY;
- if (len2 < k_ClipperEpsilon)
- return 0; // degenerate segment
- double s0pX = point.X - s0.X;
- double s0pY = point.Y - s0.Y;
- var dot = s0pX * sX + s0pY * sY;
- return Mathf.Clamp01((float) (dot / len2));
- }
-
- bool DoesIntersectOriginal(IntPoint l1, IntPoint l2)
- {
- foreach (var original in m_OriginalPolygon)
- {
- var numPoints = original.Count;
- for (var i = 0; i < numPoints; ++i)
- {
- if (FindIntersection(l1, l2, original[i], original[(i + 1) % numPoints]) == 2)
- {
- return true;
- }
- }
- }
- return false;
- }
- }
- #if UNITY_EDITOR
- // Used by inspector to draw the baked path
- List<List<Vector2>> m_Vector2Path;
- public List<List<Vector2>> GetBakedPath()
- {
- // Convert to client space
- var numPaths = m_Solution.Count;
- if (m_Vector2Path == null)
- {
- m_Vector2Path = new List<List<Vector2>>(numPaths);
- for (int i = 0; i < numPaths; ++i)
- {
- var srcPoly = m_Solution[i];
- int numPoints = srcPoly.Count;
- var pathSegment = new List<Vector2>(numPoints);
- for (int j = 0; j < numPoints; j++)
- {
- // Restore the original aspect ratio
- pathSegment.Add(m_AspectStretcher.Unstretch(
- new Vector2(srcPoly[j].X, srcPoly[j].Y) * k_IntToFloatScaler));
- }
- m_Vector2Path.Add(pathSegment);
- }
- }
- return m_Vector2Path;
- }
- #endif
- static int FindIntersection(in IntPoint p1, in IntPoint p2, in IntPoint p3, in IntPoint p4)
- {
- // Get the segments' parameters.
- double dx12 = p2.X - p1.X;
- double dy12 = p2.Y - p1.Y;
- double dx34 = p4.X - p3.X;
- double dy34 = p4.Y - p3.Y;
- // Solve for t1 and t2
- double denominator = dy12 * dx34 - dx12 * dy34;
- double t1 =
- ((p1.X - p3.X) * dy34 + (p3.Y - p1.Y) * dx34)
- / denominator;
- if (double.IsInfinity(t1) || double.IsNaN(t1))
- {
- // The lines are parallel (or close enough to it).
- if (IntPointDiffSqrMagnitude(p1, p3) < k_ClipperEpsilon ||
- IntPointDiffSqrMagnitude(p1, p4) < k_ClipperEpsilon ||
- IntPointDiffSqrMagnitude(p2, p3) < k_ClipperEpsilon ||
- IntPointDiffSqrMagnitude(p2, p4) < k_ClipperEpsilon)
- {
- return 2; // they are the same line, or very close parallels
- }
- return 0; // no intersection
- }
- double t2 = ((p3.X - p1.X) * dy12 + (p1.Y - p3.Y) * dx12) / -denominator;
- return (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 < 1) ? 2 : 1; // 2 = segments intersect, 1 = lines intersect
-
- // local function
- double IntPointDiffSqrMagnitude(IntPoint point1, IntPoint point2)
- {
- double x = point1.X - point2.X;
- double y = point1.Y - point2.Y;
- return x * x + y * y;
- }
- }
- }
- readonly struct AspectStretcher
- {
- public float Aspect { get; }
- readonly float m_InverseAspect;
- readonly float m_CenterX;
- public AspectStretcher(float aspect, float centerX)
- {
- Aspect = aspect;
- m_InverseAspect = 1 / Aspect;
- m_CenterX = centerX;
- }
- public Vector2 Stretch(Vector2 p) => new Vector2((p.x - m_CenterX) * m_InverseAspect + m_CenterX, p.y);
- public Vector2 Unstretch(Vector2 p) => new Vector2((p.x - m_CenterX) * Aspect + m_CenterX, p.y);
- }
- float m_MinFrustumHeightWithBones;
- List<List<IntPoint>> m_OriginalPolygon;
- IntPoint m_MidPoint;
- List<List<IntPoint>> m_Skeleton = new List<List<IntPoint>>();
- const long k_FloatToIntScaler = 100000;
- const float k_IntToFloatScaler = 1.0f / k_FloatToIntScaler;
- const float k_MinStepSize = 0.005f;
- Rect m_PolygonRect;
- AspectStretcher m_AspectStretcher = new AspectStretcher(1, 0);
- float m_MaxComputationTimeForFullSkeletonBakeInSeconds = 5f;
- public ConfinerOven(in List<List<Vector2>> inputPath, in float aspectRatio, float maxFrustumHeight)
- {
- Initialize(inputPath, aspectRatio, maxFrustumHeight);
- }
- /// <summary>
- /// Converts and returns a prebaked ConfinerState for the input frustumHeight.
- /// </summary>
- public BakedSolution GetBakedSolution(float frustumHeight)
- {
- // If the user has set a max frustum height, respect it
- frustumHeight = m_Cache.userSetMaxFrustumHeight <= 0
- ? frustumHeight
- : Mathf.Min(m_Cache.userSetMaxFrustumHeight, frustumHeight);
-
- // Special case: we are shrank to the mid point of the original input confiner area.
- if (State == BakingState.BAKED && frustumHeight > m_Cache.theoriticalMaxFrustumHeight)
- {
- return new BakedSolution(
- m_AspectStretcher.Aspect, frustumHeight, false,
- m_PolygonRect, m_OriginalPolygon,
- new List<List<IntPoint>>{new List<IntPoint> { m_MidPoint }});
- }
- // Inflate with clipper to frustumHeight
- var offsetter = new ClipperOffset();
- offsetter.AddPaths(m_OriginalPolygon, JoinType.jtMiter, EndType.etClosedPolygon);
- var solution = new List<List<IntPoint>>();
- offsetter.Execute(ref solution, -1f * frustumHeight * k_FloatToIntScaler);
- // Add in the skeleton
- var bakedSolution = new List<List<IntPoint>>();
- if (State == BakingState.BAKING || m_Skeleton.Count == 0)
- {
- bakedSolution = solution;
- }
- else
- {
- var c = new Clipper();
- c.AddPaths(solution, PolyType.ptSubject, true);
- c.AddPaths(m_Skeleton, PolyType.ptClip, true);
- c.Execute(ClipType.ctUnion, bakedSolution, PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd);
- }
- return new BakedSolution(
- m_AspectStretcher.Aspect, frustumHeight,
- m_MinFrustumHeightWithBones < frustumHeight,
- m_PolygonRect, m_OriginalPolygon, bakedSolution);
- }
- struct PolygonSolution
- {
- public List<List<IntPoint>> polygons;
- public float frustumHeight;
- public bool StateChanged(in List<List<IntPoint>> paths)
- {
- if (paths.Count != polygons.Count)
- return true;
- for (var i = 0; i < paths.Count; ++i)
- {
- if (paths[i].Count != polygons[i].Count)
- return true;
- }
- return false;
- }
- public bool IsNull => polygons == null;
- }
- public enum BakingState
- {
- BAKING, BAKED, TIMEOUT
- }
- public BakingState State { get; private set; }
- public float bakeProgress;
- struct BakingStateCache
- {
- public ClipperOffset offsetter;
- public List<PolygonSolution> solutions;
- public PolygonSolution rightCandidate;
- public PolygonSolution leftCandidate;
- public List<List<IntPoint>> maxCandidate;
- public float stepSize;
- public float maxFrustumHeight;
- public float userSetMaxFrustumHeight;
- public float theoriticalMaxFrustumHeight;
- public float currentFrustumHeight;
- public float bakeTime;
- }
- BakingStateCache m_Cache;
- void Initialize(in List<List<Vector2>> inputPath, in float aspectRatio, float maxFrustumHeight)
- {
- m_Skeleton.Clear();
- m_Cache.userSetMaxFrustumHeight = maxFrustumHeight;
- m_MinFrustumHeightWithBones = float.MaxValue;
- // calculate mid point and use it as the most shrank down version
- m_PolygonRect = GetPolygonBoundingBox(inputPath);
- m_AspectStretcher = new AspectStretcher(aspectRatio, m_PolygonRect.center.x);
-
- // Don't compute further than what is the theoretical max
- m_Cache.theoriticalMaxFrustumHeight = Mathf.Max(m_PolygonRect.width / aspectRatio, m_PolygonRect.height) / 2f;
- // Initialize clipper
- m_OriginalPolygon = new List<List<IntPoint>>(inputPath.Count);
- for (var i = 0; i < inputPath.Count; ++i)
- {
- var srcPath = inputPath[i];
- var numPoints = srcPath.Count;
- var path = new List<IntPoint>(numPoints);
- for (var j = 0; j < numPoints; ++j)
- {
- // Neutralize the aspect ratio
- var p = m_AspectStretcher.Stretch(srcPath[j]);
- path.Add(new IntPoint(p.x * k_FloatToIntScaler, p.y * k_FloatToIntScaler));
- }
- m_OriginalPolygon.Add(path);
- }
- m_MidPoint = MidPointOfIntRect(ClipperBase.GetBounds(m_OriginalPolygon));
- // Skip the expensive skeleton calculation if it's not wanted (oversized window off)
- if (m_Cache.userSetMaxFrustumHeight < 0)
- {
- State = BakingState.BAKED; // if we don't need skeleton, then we don't need to bake
- return;
- }
- // exact comparison to 0 is intentional!
- m_Cache.maxFrustumHeight = m_Cache.userSetMaxFrustumHeight;
- if (m_Cache.maxFrustumHeight == 0 || m_Cache.maxFrustumHeight > m_Cache.theoriticalMaxFrustumHeight)
- {
- m_Cache.maxFrustumHeight = m_Cache.theoriticalMaxFrustumHeight;
- }
- m_Cache.stepSize = m_Cache.maxFrustumHeight;
- // Binary search for state changes so we can compute the skeleton
- m_Cache.offsetter = new ClipperOffset();
- m_Cache.offsetter.AddPaths(m_OriginalPolygon, JoinType.jtMiter, EndType.etClosedPolygon);
- var solution = new List<List<IntPoint>>();
- m_Cache.offsetter.Execute(ref solution, 0);
-
- m_Cache.solutions = new List<PolygonSolution>();
- m_Cache.solutions.Add(new PolygonSolution
- {
- polygons = solution,
- frustumHeight = 0,
- });
- m_Cache.rightCandidate = new PolygonSolution();
- m_Cache.leftCandidate = new PolygonSolution
- {
- polygons = solution,
- frustumHeight = 0,
- };
- m_Cache.currentFrustumHeight = 0;
- m_Cache.maxCandidate = new List<List<IntPoint>>();
- m_Cache.offsetter.Execute(ref m_Cache.maxCandidate, -1f * m_Cache.theoriticalMaxFrustumHeight * k_FloatToIntScaler);
- m_Cache.bakeTime = 0;
- State = BakingState.BAKING;
- bakeProgress = 0;
-
- // local functions
- Rect GetPolygonBoundingBox(in List<List<Vector2>> polygons)
- {
- float minX = float.PositiveInfinity, maxX = float.NegativeInfinity;
- float minY = float.PositiveInfinity, maxY = float.NegativeInfinity;
- for (var i = 0; i < polygons.Count; ++i)
- {
- for (var j = 0; j < polygons[i].Count; ++j)
- {
- var p = polygons[i][j];
- minX = Mathf.Min(minX, p.x);
- maxX = Mathf.Max(maxX, p.x);
- minY = Mathf.Min(minY, p.y);
- maxY = Mathf.Max(maxY, p.y);
- }
- }
- return new Rect(minX, minY, Mathf.Max(0, maxX - minX), Mathf.Max(0, maxY - minY));
- }
- IntPoint MidPointOfIntRect(IntRect bounds) =>
- new IntPoint((bounds.left + bounds.right) / 2, (bounds.top + bounds.bottom) / 2);
- }
-
- /// <summary>
- /// Creates shrinkable polygons from input parameters.
- /// The algorithm is divide and conquer. It iteratively shrinks down the input
- /// polygon towards its shrink directions. If the polygon intersects with itself,
- /// then we divide the polygon into two polygons at the intersection point, and
- /// continue the algorithm on these two polygons separately. We need to keep track of
- /// the connectivity information between sub-polygons.
- /// </summary>
- public void BakeConfiner(float maxComputationTimePerFrameInSeconds)
- {
- if (State != BakingState.BAKING)
- return;
-
- var startTime = Time.realtimeSinceStartup;
-
- while (m_Cache.solutions.Count < 1000)
- {
- var numPaths = m_Cache.leftCandidate.polygons.Count;
- var candidate = new List<List<IntPoint>>(numPaths);
- m_Cache.stepSize = Mathf.Min(m_Cache.stepSize,
- m_Cache.maxFrustumHeight - m_Cache.leftCandidate.frustumHeight);
- #if false
- Debug.Log($"States = {solutions.Count}, "
- + $"Frustum height = {currentFrustumHeight}, stepSize = {stepSize}");
- #endif
- m_Cache.currentFrustumHeight =
- m_Cache.leftCandidate.frustumHeight + m_Cache.stepSize;
- if (Math.Abs(m_Cache.currentFrustumHeight - m_Cache.maxFrustumHeight) <
- UnityVectorExtensions.Epsilon)
- {
- candidate = m_Cache.maxCandidate;
- }
- else
- {
- m_Cache.offsetter.Execute(
- ref candidate, -1f * m_Cache.currentFrustumHeight * k_FloatToIntScaler);
- }
-
- if (m_Cache.leftCandidate.StateChanged(in candidate))
- {
- m_Cache.rightCandidate = new PolygonSolution
- {
- polygons = candidate,
- frustumHeight = m_Cache.currentFrustumHeight,
- };
- m_Cache.stepSize = Mathf.Max(m_Cache.stepSize / 2f, k_MinStepSize);
- }
- else
- {
- m_Cache.leftCandidate = new PolygonSolution
- {
- polygons = candidate,
- frustumHeight = m_Cache.currentFrustumHeight,
- };
- // if we have not found right yet, then we don't need to decrease stepsize
- if (!m_Cache.rightCandidate.IsNull)
- {
- m_Cache.stepSize = Mathf.Max(m_Cache.stepSize / 2f, k_MinStepSize);
- }
- }
-
- // if we have a right candidate, and left and right are sufficiently close,
- // then we have located a state change point
- if (!m_Cache.rightCandidate.IsNull && m_Cache.stepSize <= k_MinStepSize)
- {
- // Add both states: one before the state change and one after
- m_Cache.solutions.Add(m_Cache.leftCandidate);
- m_Cache.solutions.Add(m_Cache.rightCandidate);
- m_Cache.leftCandidate = m_Cache.rightCandidate;
- m_Cache.rightCandidate = new PolygonSolution();
-
- // Back to max step
- m_Cache.stepSize = m_Cache.maxFrustumHeight;
- }
- else if (m_Cache.rightCandidate.IsNull ||
- m_Cache.leftCandidate.frustumHeight >= m_Cache.maxFrustumHeight)
- {
- m_Cache.solutions.Add(m_Cache.leftCandidate);
- break; // stop searching, because we are at the bound
- }
- // Pause after max time per iteration reached
- var elapsedTime = Time.realtimeSinceStartup - startTime;
- if (elapsedTime > maxComputationTimePerFrameInSeconds)
- {
- m_Cache.bakeTime += elapsedTime;
- if (m_Cache.bakeTime > m_MaxComputationTimeForFullSkeletonBakeInSeconds)
- {
- State = BakingState.TIMEOUT;
- }
-
- bakeProgress = m_Cache.leftCandidate.frustumHeight / m_Cache.maxFrustumHeight;
- return;
- }
- }
- ComputeSkeleton(in m_Cache.solutions);
- // Remove useless/empty results
- for (var i = m_Cache.solutions.Count - 1; i >= 0; --i)
- {
- if (m_Cache.solutions[i].polygons.Count == 0)
- {
- m_Cache.solutions.RemoveAt(i);
- }
- }
- bakeProgress = 1;
- State = BakingState.BAKED;
- void ComputeSkeleton(in List<PolygonSolution> solutions)
- {
- // At each state change point, collect geometry that gets lost over the transition
- var clipper = new Clipper();
- var offsetter = new ClipperOffset();
- for (int i = 1; i < solutions.Count - 1; i += 2)
- {
- var prev = solutions[i];
- var next = solutions[i+1];
- const int padding = 5; // to counteract precision problems - inflates small regions
- double step = padding * k_FloatToIntScaler * (next.frustumHeight - prev.frustumHeight);
- // Grow the larger polygon to inflate marginal regions
- var expandedPrev = new List<List<IntPoint>>();
- offsetter.Clear();
- offsetter.AddPaths(prev.polygons, JoinType.jtMiter, EndType.etClosedPolygon);
- offsetter.Execute(ref expandedPrev, step);
- // Grow the smaller polygon to be a bit bigger than the expanded larger one
- var expandedNext = new List<List<IntPoint>>();
- offsetter.Clear();
- offsetter.AddPaths(next.polygons, JoinType.jtMiter, EndType.etClosedPolygon);
- offsetter.Execute(ref expandedNext, step * 2);
- // Compute the difference - this is the lost geometry
- var solution = new List<List<IntPoint>>();
- clipper.Clear();
- clipper.AddPaths(expandedPrev, PolyType.ptSubject, true);
- clipper.AddPaths(expandedNext, PolyType.ptClip, true);
- clipper.Execute(ClipType.ctDifference, solution, PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd);
- // Add that lost geometry to the skeleton
- if (solution.Count > 0 && solution[0].Count > 0)
- {
- m_Skeleton.AddRange(solution);
- // Exact comparison is intentional
- if (m_MinFrustumHeightWithBones == float.MaxValue)
- m_MinFrustumHeightWithBones = next.frustumHeight;
- }
- }
- }
- }
- }
- }
|