| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- #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
- /// <summary>
- /// <para>
- /// 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.
- /// </para>
- ///
- /// <para>
- /// 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.
- /// </para>
- ///
- /// <para>
- /// The cached secondary polygon needs to be recomputed in the following circumstances:
- /// <list type="bullet">
- /// <item>when the input polygon's points change</item>
- /// <item>when the input polygon is non-uniformly scaled</item>
- /// <item>when the input polygon is rotated</item>
- /// </list>
- /// 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.
- /// </para>
- ///
- /// <para>
- /// 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.
- /// </para>
- ///
- /// <para>
- /// 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.
- /// </para>
- /// </summary>
- [AddComponentMenu("")] // Hide in menu
- [SaveDuringPlay]
- [ExecuteAlways]
- [DisallowMultipleComponent]
- [HelpURL(Documentation.BaseURL + "manual/CinemachineConfiner2D.html")]
- public class CinemachineConfiner2D : CinemachineExtension
- {
- /// <summary>The 2D shape within which the camera is to be contained.</summary>
- [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;
- /// <summary>Damping applied automatically around corners to avoid jumps.</summary>
- [Tooltip("Damping applied around corners to avoid jumps. Higher numbers are more gradual.")]
- [Range(0, 5)]
- public float m_Damping;
- /// <summary>
- /// 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.
- /// </summary>
- [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;
- /// <summary>Invalidates cache and consequently trigger a rebake at next iteration.</summary>
- public void InvalidateCache()
- {
- m_shapeCache.Invalidate();
- }
- /// <summary>Validates cache</summary>
- /// <param name="cameraAspectRatio">Aspect ratio of camera.</param>
- /// <returns>Returns true if the cache could be validated. False, otherwise.</returns>
- public bool ValidateCache(float cameraAspectRatio)
- {
- return m_shapeCache.ValidateCache(m_BoundingShape2D, m_MaxWindowSize, cameraAspectRatio, out _);
- }
- const float k_cornerAngleTreshold = 10f;
-
- /// <summary>
- /// Callback to do the camera confining
- /// </summary>
- /// <param name="vcam">The virtual camera being processed</param>
- /// <param name="stage">The current pipeline stage</param>
- /// <param name="state">The current virtual camera state</param>
- /// <param name="deltaTime">The current applicable deltaTime</param>
- 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<VcamExtraState>(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;
- }
- }
- /// <summary>
- /// Calculates half frustum height for orthographic or perspective camera.
- /// For more info on frustum height, see <see cref="docs.unity3d.com/Manual/FrustumSizeAtDistance.html"/>
- /// </summary>
- /// <param name="state">CameraState for checking if Orthographic or Perspective</param>
- /// <param name="vcam">vcam, to check its position</param>
- /// <returns>Frustum height of the camera</returns>
- 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;
- /// <summary>
- /// ShapeCache: contains all states that dependent only on the settings in the confiner.
- /// </summary>
- struct ShapeCache
- {
- public ConfinerOven m_confinerOven;
- public List<List<Vector2>> 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;
- /// <summary>
- /// Invalidates shapeCache
- /// </summary>
- public void Invalidate()
- {
- m_aspectRatio = 0;
- m_maxWindowSize = -1;
- m_DeltaBakedToWorld = m_DeltaWorldToBaked = Matrix4x4.identity;
- m_boundingShape2D = null;
- m_OriginalPath = null;
- m_confinerOven = null;
- }
-
- /// <summary>
- /// Checks if we have a valid confiner state cache. Calculates cache if it is invalid (outdated or empty).
- /// </summary>
- /// <param name="boundingShape2D">Bounding shape</param>
- /// <param name="maxWindowSize">Max Window size</param>
- /// <param name="aspectRatio">Aspect ratio/param>
- /// <param name="confinerStateChanged">True, if the baked confiner state has changed.
- /// False, otherwise.</param>
- /// <returns>True, if input is valid. False, otherwise.</returns>
- 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<List<Vector2>>();
- // Cache the current worldspace shape
- m_bakedToWorld = boundingShape2D.transform.localToWorldMatrix;
- for (int i = 0; i < poly.pathCount; ++i)
- {
- Vector2[] path = poly.GetPath(i);
- List<Vector2> dst = new List<Vector2>();
- 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<List<Vector2>>();
- // 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<Vector2> dst = new List<Vector2>();
- 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<List<Vector2>> originalPath,
- ref List<List<Vector2>> currentPath,
- out Matrix4x4 pathLocalToWorld)
- {
- originalPath = m_shapeCache.m_OriginalPath;
- pathLocalToWorld = m_shapeCache.m_DeltaBakedToWorld;
- currentPath.Clear();
- var allExtraStates = GetAllExtraStates<VcamExtraState>();
- 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
- }
|