Rasagar/Library/PackageCache/com.unity.cinemachine/Runtime/Core/ConfinerOven.cs
2024-08-26 23:07:20 +03:00

605 lines
26 KiB
C#

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;
}
}
}
}
}
}