#if !UNITY_2019_3_OR_NEWER
#define CINEMACHINE_PHYSICS_2D
#endif
using System;
using System.Collections.Generic;
using Cinemachine.Utility;
using UnityEditor;
using UnityEngine;
namespace Cinemachine
{
#if CINEMACHINE_PHYSICS_2D
///
///
/// An add-on module for Cinemachine Virtual Camera that post-processes the final position
/// of the virtual camera. It will confine the camera's position such that the screen edges stay
/// within a shape defined by a 2D polygon. This will work for orthographic or perspective cameras,
/// provided that the camera's forward vector remains parallel to the bounding shape's normal,
/// i.e. that the camera is looking straight at the polygon, and not obliquely at it.
///
///
///
/// When confining the camera, the camera's view size at the polygon plane is considered, and
/// also its aspect ratio. Based on this information and the input polygon, a second (smaller)
/// polygon is computed to which the camera's transform is constrained. Computation of this secondary
/// polygon is nontrivial and expensive, so it should be done only when absolutely necessary.
///
///
///
/// The cached secondary polygon needs to be recomputed in the following circumstances:
///
/// - when the input polygon's points change
/// - when the input polygon is non-uniformly scaled
/// - when the input polygon is rotated
///
/// For efficiency reasons, Cinemachine will not automatically regenerate the inner polygon
/// in these cases, and it is the responsibility of the client to call the InvalidateCache()
/// method to trigger the recalculation. An inspector button is also provided for this purpose.
///
///
///
/// If the input polygon scales uniformly or translates, the cache remains valid. If the
/// polygon rotates, then the cache degrades in quality (more or less depending on the aspect
/// ratio - it's better if the ratio is close to 1:1) but can still be used.
/// Regenerating it will eliminate the imperfections.
///
///
///
/// The cached secondary polygon is not a single polygon, but rather a family of polygons from
/// which a member is chosen depending on the current size of the camera view. The number of
/// polygons in this family will depend on the complexity of the input polygon, and the maximum
/// expected camera view size. The MaxOrthoSize property is provided to give a hint to the
/// algorithm to stop generating polygons for camera view sizes larger than the one specified.
/// This can represent a substantial cost saving when regenerating the cache, so it is a good
/// idea to set it carefully. Leaving it at 0 will cause the maximum number of polygons to be generated.
///
///
[AddComponentMenu("")] // Hide in menu
[SaveDuringPlay]
[ExecuteAlways]
[DisallowMultipleComponent]
[HelpURL(Documentation.BaseURL + "manual/CinemachineConfiner2D.html")]
public class CinemachineConfiner2D : CinemachineExtension
{
/// The 2D shape within which the camera is to be contained.
[Tooltip("The 2D shape within which the camera is to be contained. " +
"Can be a 2D polygon or 2D composite collider.")]
public Collider2D m_BoundingShape2D;
/// Damping applied automatically around corners to avoid jumps.
[Tooltip("Damping applied around corners to avoid jumps. Higher numbers are more gradual.")]
[Range(0, 5)]
public float m_Damping;
///
/// To optimize computation and memory costs, set this to the largest view size that the camera
/// is expected to have. The confiner will not compute a polygon cache for frustum sizes larger
/// than this. This refers to the size in world units of the frustum at the confiner plane
/// (for orthographic cameras, this is just the orthographic size). If set to 0, then this
/// parameter is ignored and a polygon cache will be calculated for all potential window sizes.
///
[Tooltip("To optimize computation and memory costs, set this to the largest view size that the "
+ "camera is expected to have. The confiner will not compute a polygon cache for frustum "
+ "sizes larger than this. This refers to the size in world units of the frustum at the "
+ "confiner plane (for orthographic cameras, this is just the orthographic size). If set "
+ "to 0, then this parameter is ignored and a polygon cache will be calculated for all "
+ "potential window sizes.")]
public float m_MaxWindowSize;
float m_MaxComputationTimePerFrameInSeconds = 1f / 120f;
/// Invalidates cache and consequently trigger a rebake at next iteration.
public void InvalidateCache()
{
m_shapeCache.Invalidate();
}
/// Validates cache
/// Aspect ratio of camera.
/// Returns true if the cache could be validated. False, otherwise.
public bool ValidateCache(float cameraAspectRatio)
{
return m_shapeCache.ValidateCache(m_BoundingShape2D, m_MaxWindowSize, cameraAspectRatio, out _);
}
const float k_cornerAngleTreshold = 10f;
///
/// Callback to do the camera confining
///
/// The virtual camera being processed
/// The current pipeline stage
/// The current virtual camera state
/// The current applicable deltaTime
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
if (stage == CinemachineCore.Stage.Body)
{
var aspectRatio = state.Lens.Aspect;
if (!m_shapeCache.ValidateCache(
m_BoundingShape2D, m_MaxWindowSize, aspectRatio, out bool confinerStateChanged))
{
return; // invalid path
}
var oldCameraPos = state.CorrectedPosition;
var cameraPosLocal = m_shapeCache.m_DeltaWorldToBaked.MultiplyPoint3x4(oldCameraPos);
var currentFrustumHeight = CalculateHalfFrustumHeight(state, cameraPosLocal.z);
// convert frustum height from world to baked space. deltaWorldToBaked.lossyScale is always uniform.
var bakedSpaceFrustumHeight = currentFrustumHeight *
m_shapeCache.m_DeltaWorldToBaked.lossyScale.x;
// Make sure we have a solution for our current frustum size
var extra = GetExtraState(vcam);
extra.m_vcam = vcam;
if (confinerStateChanged || extra.m_BakedSolution == null
|| !extra.m_BakedSolution.IsValid())
{
extra.m_BakedSolution = m_shapeCache.m_confinerOven.GetBakedSolution(bakedSpaceFrustumHeight);
}
cameraPosLocal = extra.m_BakedSolution.ConfinePoint(cameraPosLocal);
var newCameraPos = m_shapeCache.m_DeltaBakedToWorld.MultiplyPoint3x4(cameraPosLocal);
// Don't move the camera along its z-axis
var fwd = state.CorrectedOrientation * Vector3.forward;
newCameraPos -= fwd * Vector3.Dot(fwd, newCameraPos - oldCameraPos);
// Remember the desired displacement for next frame
var prev = extra.m_PreviousDisplacement;
var displacement = newCameraPos - oldCameraPos;
extra.m_PreviousDisplacement = displacement;
if (!VirtualCamera.PreviousStateIsValid || deltaTime < 0 || m_Damping <= 0)
extra.m_DampedDisplacement = Vector3.zero;
else
{
// If a big change from previous frame's desired displacement is detected,
// assume we are going around a corner and extract that difference for damping
if (prev.sqrMagnitude > 0.01f && Vector2.Angle(prev, displacement) > k_cornerAngleTreshold)
extra.m_DampedDisplacement += displacement - prev;
extra.m_DampedDisplacement -= Damper.Damp(extra.m_DampedDisplacement, m_Damping, deltaTime);
displacement -= extra.m_DampedDisplacement;
}
state.PositionCorrection += displacement;
}
}
///
/// Calculates half frustum height for orthographic or perspective camera.
/// For more info on frustum height, see
///
/// CameraState for checking if Orthographic or Perspective
/// vcam, to check its position
/// Frustum height of the camera
float CalculateHalfFrustumHeight(in CameraState state, in float cameraPosLocalZ)
{
float frustumHeight;
if (state.Lens.Orthographic)
{
frustumHeight = state.Lens.OrthographicSize;
}
else
{
// distance between the collider's plane and the camera
float distance = cameraPosLocalZ;
frustumHeight = distance * Mathf.Tan(state.Lens.FieldOfView * 0.5f * Mathf.Deg2Rad);
}
return Mathf.Abs(frustumHeight);
}
class VcamExtraState
{
public Vector3 m_PreviousDisplacement;
public Vector3 m_DampedDisplacement;
public ConfinerOven.BakedSolution m_BakedSolution;
public CinemachineVirtualCameraBase m_vcam;
};
ShapeCache m_shapeCache;
///
/// ShapeCache: contains all states that dependent only on the settings in the confiner.
///
struct ShapeCache
{
public ConfinerOven m_confinerOven;
public List> m_OriginalPath; // in baked space, not including offset
// These account for offset and transform change since baking
public Matrix4x4 m_DeltaWorldToBaked;
public Matrix4x4 m_DeltaBakedToWorld;
float m_aspectRatio;
float m_maxWindowSize;
internal float m_maxComputationTimePerFrameInSeconds;
Matrix4x4 m_bakedToWorld; // defines baked space
Collider2D m_boundingShape2D;
///
/// Invalidates shapeCache
///
public void Invalidate()
{
m_aspectRatio = 0;
m_maxWindowSize = -1;
m_DeltaBakedToWorld = m_DeltaWorldToBaked = Matrix4x4.identity;
m_boundingShape2D = null;
m_OriginalPath = null;
m_confinerOven = null;
}
///
/// Checks if we have a valid confiner state cache. Calculates cache if it is invalid (outdated or empty).
///
/// Bounding shape
/// Max Window size
/// Aspect ratio/param>
/// True, if the baked confiner state has changed.
/// False, otherwise.
/// True, if input is valid. False, otherwise.
public bool ValidateCache(
Collider2D boundingShape2D, float maxWindowSize,
float aspectRatio, out bool confinerStateChanged)
{
confinerStateChanged = false;
if (IsValid(boundingShape2D, aspectRatio, maxWindowSize))
{
// Advance confiner baking
if (m_confinerOven.State == ConfinerOven.BakingState.BAKING)
{
m_confinerOven.BakeConfiner(m_maxComputationTimePerFrameInSeconds);
// If no longer baking, then confinerStateChanged
confinerStateChanged = m_confinerOven.State != ConfinerOven.BakingState.BAKING;
}
// Update in case the polygon's transform changed
CalculateDeltaTransformationMatrix();
// If delta world to baked scale is uniform, cache is valid.
Vector2 lossyScaleXY = m_DeltaWorldToBaked.lossyScale;
if (lossyScaleXY.IsUniform())
{
return true;
}
}
Invalidate();
confinerStateChanged = true;
Type colliderType = boundingShape2D == null ? null: boundingShape2D.GetType();
if (colliderType == typeof(PolygonCollider2D))
{
var poly = boundingShape2D as PolygonCollider2D;
m_OriginalPath = new List>();
// Cache the current worldspace shape
m_bakedToWorld = boundingShape2D.transform.localToWorldMatrix;
for (int i = 0; i < poly.pathCount; ++i)
{
Vector2[] path = poly.GetPath(i);
List dst = new List();
for (int j = 0; j < path.Length; ++j)
dst.Add(m_bakedToWorld.MultiplyPoint3x4(path[j]));
m_OriginalPath.Add(dst);
}
}
else if (colliderType == typeof(CompositeCollider2D))
{
var poly = boundingShape2D as CompositeCollider2D;
m_OriginalPath = new List>();
// Cache the current worldspace shape
m_bakedToWorld = boundingShape2D.transform.localToWorldMatrix;
var path = new Vector2[poly.pointCount];
for (int i = 0; i < poly.pathCount; ++i)
{
int numPoints = poly.GetPath(i, path);
List dst = new List();
for (int j = 0; j < numPoints; ++j)
dst.Add(m_bakedToWorld.MultiplyPoint3x4(path[j]));
m_OriginalPath.Add(dst);
}
}
else
{
return false; // input collider is invalid
}
m_confinerOven = new ConfinerOven(m_OriginalPath, aspectRatio, maxWindowSize);
m_aspectRatio = aspectRatio;
m_boundingShape2D = boundingShape2D;
m_maxWindowSize = maxWindowSize;
CalculateDeltaTransformationMatrix();
return true;
}
bool IsValid(in Collider2D boundingShape2D, in float aspectRatio, in float maxOrthoSize)
{
return boundingShape2D != null && m_boundingShape2D != null &&
m_boundingShape2D == boundingShape2D && // same boundingShape?
m_OriginalPath != null && // first time?
m_confinerOven != null && // cache not empty?
Mathf.Abs(m_aspectRatio - aspectRatio) < UnityVectorExtensions.Epsilon && // aspect changed?
Mathf.Abs(m_maxWindowSize - maxOrthoSize) < UnityVectorExtensions.Epsilon; // max ortho changed?
}
void CalculateDeltaTransformationMatrix()
{
// Account for current collider offset (in local space) and
// incorporate the worldspace delta that the confiner has moved since baking
var m = Matrix4x4.Translate(-m_boundingShape2D.offset) *
m_boundingShape2D.transform.worldToLocalMatrix;
m_DeltaWorldToBaked = m_bakedToWorld * m;
m_DeltaBakedToWorld = m_DeltaWorldToBaked.inverse;
}
}
#if UNITY_EDITOR
// Used by editor gizmo drawer
internal bool GetGizmoPaths(
out List> originalPath,
ref List> currentPath,
out Matrix4x4 pathLocalToWorld)
{
originalPath = m_shapeCache.m_OriginalPath;
pathLocalToWorld = m_shapeCache.m_DeltaBakedToWorld;
currentPath.Clear();
var allExtraStates = GetAllExtraStates();
for (var i = 0; i < allExtraStates.Count; ++i)
{
var e = allExtraStates[i];
if (e.m_BakedSolution != null)
{
currentPath.AddRange(e.m_BakedSolution.GetBakedPath());
}
}
return originalPath != null;
}
internal float BakeProgress() => m_shapeCache.m_confinerOven != null ? m_shapeCache.m_confinerOven.bakeProgress : 0f;
internal bool ConfinerOvenTimedOut() => m_shapeCache.m_confinerOven != null &&
m_shapeCache.m_confinerOven.State == ConfinerOven.BakingState.TIMEOUT;
#endif
void OnValidate()
{
m_Damping = Mathf.Max(0, m_Damping);
m_shapeCache.m_maxComputationTimePerFrameInSeconds = m_MaxComputationTimePerFrameInSeconds;
}
void Reset()
{
m_Damping = 0.5f;
m_MaxWindowSize = -1;
}
}
#endif
}