CinemachineConfiner2D.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #if !UNITY_2019_3_OR_NEWER
  2. #define CINEMACHINE_PHYSICS_2D
  3. #endif
  4. using System;
  5. using System.Collections.Generic;
  6. using Cinemachine.Utility;
  7. using UnityEditor;
  8. using UnityEngine;
  9. namespace Cinemachine
  10. {
  11. #if CINEMACHINE_PHYSICS_2D
  12. /// <summary>
  13. /// <para>
  14. /// An add-on module for Cinemachine Virtual Camera that post-processes the final position
  15. /// of the virtual camera. It will confine the camera's position such that the screen edges stay
  16. /// within a shape defined by a 2D polygon. This will work for orthographic or perspective cameras,
  17. /// provided that the camera's forward vector remains parallel to the bounding shape's normal,
  18. /// i.e. that the camera is looking straight at the polygon, and not obliquely at it.
  19. /// </para>
  20. ///
  21. /// <para>
  22. /// When confining the camera, the camera's view size at the polygon plane is considered, and
  23. /// also its aspect ratio. Based on this information and the input polygon, a second (smaller)
  24. /// polygon is computed to which the camera's transform is constrained. Computation of this secondary
  25. /// polygon is nontrivial and expensive, so it should be done only when absolutely necessary.
  26. /// </para>
  27. ///
  28. /// <para>
  29. /// The cached secondary polygon needs to be recomputed in the following circumstances:
  30. /// <list type="bullet">
  31. /// <item>when the input polygon's points change</item>
  32. /// <item>when the input polygon is non-uniformly scaled</item>
  33. /// <item>when the input polygon is rotated</item>
  34. /// </list>
  35. /// For efficiency reasons, Cinemachine will not automatically regenerate the inner polygon
  36. /// in these cases, and it is the responsibility of the client to call the InvalidateCache()
  37. /// method to trigger the recalculation. An inspector button is also provided for this purpose.
  38. /// </para>
  39. ///
  40. /// <para>
  41. /// If the input polygon scales uniformly or translates, the cache remains valid. If the
  42. /// polygon rotates, then the cache degrades in quality (more or less depending on the aspect
  43. /// ratio - it's better if the ratio is close to 1:1) but can still be used.
  44. /// Regenerating it will eliminate the imperfections.
  45. /// </para>
  46. ///
  47. /// <para>
  48. /// The cached secondary polygon is not a single polygon, but rather a family of polygons from
  49. /// which a member is chosen depending on the current size of the camera view. The number of
  50. /// polygons in this family will depend on the complexity of the input polygon, and the maximum
  51. /// expected camera view size. The MaxOrthoSize property is provided to give a hint to the
  52. /// algorithm to stop generating polygons for camera view sizes larger than the one specified.
  53. /// This can represent a substantial cost saving when regenerating the cache, so it is a good
  54. /// idea to set it carefully. Leaving it at 0 will cause the maximum number of polygons to be generated.
  55. /// </para>
  56. /// </summary>
  57. [AddComponentMenu("")] // Hide in menu
  58. [SaveDuringPlay]
  59. [ExecuteAlways]
  60. [DisallowMultipleComponent]
  61. [HelpURL(Documentation.BaseURL + "manual/CinemachineConfiner2D.html")]
  62. public class CinemachineConfiner2D : CinemachineExtension
  63. {
  64. /// <summary>The 2D shape within which the camera is to be contained.</summary>
  65. [Tooltip("The 2D shape within which the camera is to be contained. " +
  66. "Can be a 2D polygon or 2D composite collider.")]
  67. public Collider2D m_BoundingShape2D;
  68. /// <summary>Damping applied automatically around corners to avoid jumps.</summary>
  69. [Tooltip("Damping applied around corners to avoid jumps. Higher numbers are more gradual.")]
  70. [Range(0, 5)]
  71. public float m_Damping;
  72. /// <summary>
  73. /// To optimize computation and memory costs, set this to the largest view size that the camera
  74. /// is expected to have. The confiner will not compute a polygon cache for frustum sizes larger
  75. /// than this. This refers to the size in world units of the frustum at the confiner plane
  76. /// (for orthographic cameras, this is just the orthographic size). If set to 0, then this
  77. /// parameter is ignored and a polygon cache will be calculated for all potential window sizes.
  78. /// </summary>
  79. [Tooltip("To optimize computation and memory costs, set this to the largest view size that the "
  80. + "camera is expected to have. The confiner will not compute a polygon cache for frustum "
  81. + "sizes larger than this. This refers to the size in world units of the frustum at the "
  82. + "confiner plane (for orthographic cameras, this is just the orthographic size). If set "
  83. + "to 0, then this parameter is ignored and a polygon cache will be calculated for all "
  84. + "potential window sizes.")]
  85. public float m_MaxWindowSize;
  86. float m_MaxComputationTimePerFrameInSeconds = 1f / 120f;
  87. /// <summary>Invalidates cache and consequently trigger a rebake at next iteration.</summary>
  88. public void InvalidateCache()
  89. {
  90. m_shapeCache.Invalidate();
  91. }
  92. /// <summary>Validates cache</summary>
  93. /// <param name="cameraAspectRatio">Aspect ratio of camera.</param>
  94. /// <returns>Returns true if the cache could be validated. False, otherwise.</returns>
  95. public bool ValidateCache(float cameraAspectRatio)
  96. {
  97. return m_shapeCache.ValidateCache(m_BoundingShape2D, m_MaxWindowSize, cameraAspectRatio, out _);
  98. }
  99. const float k_cornerAngleTreshold = 10f;
  100. /// <summary>
  101. /// Callback to do the camera confining
  102. /// </summary>
  103. /// <param name="vcam">The virtual camera being processed</param>
  104. /// <param name="stage">The current pipeline stage</param>
  105. /// <param name="state">The current virtual camera state</param>
  106. /// <param name="deltaTime">The current applicable deltaTime</param>
  107. protected override void PostPipelineStageCallback(
  108. CinemachineVirtualCameraBase vcam,
  109. CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
  110. {
  111. if (stage == CinemachineCore.Stage.Body)
  112. {
  113. var aspectRatio = state.Lens.Aspect;
  114. if (!m_shapeCache.ValidateCache(
  115. m_BoundingShape2D, m_MaxWindowSize, aspectRatio, out bool confinerStateChanged))
  116. {
  117. return; // invalid path
  118. }
  119. var oldCameraPos = state.CorrectedPosition;
  120. var cameraPosLocal = m_shapeCache.m_DeltaWorldToBaked.MultiplyPoint3x4(oldCameraPos);
  121. var currentFrustumHeight = CalculateHalfFrustumHeight(state, cameraPosLocal.z);
  122. // convert frustum height from world to baked space. deltaWorldToBaked.lossyScale is always uniform.
  123. var bakedSpaceFrustumHeight = currentFrustumHeight *
  124. m_shapeCache.m_DeltaWorldToBaked.lossyScale.x;
  125. // Make sure we have a solution for our current frustum size
  126. var extra = GetExtraState<VcamExtraState>(vcam);
  127. extra.m_vcam = vcam;
  128. if (confinerStateChanged || extra.m_BakedSolution == null
  129. || !extra.m_BakedSolution.IsValid())
  130. {
  131. extra.m_BakedSolution = m_shapeCache.m_confinerOven.GetBakedSolution(bakedSpaceFrustumHeight);
  132. }
  133. cameraPosLocal = extra.m_BakedSolution.ConfinePoint(cameraPosLocal);
  134. var newCameraPos = m_shapeCache.m_DeltaBakedToWorld.MultiplyPoint3x4(cameraPosLocal);
  135. // Don't move the camera along its z-axis
  136. var fwd = state.CorrectedOrientation * Vector3.forward;
  137. newCameraPos -= fwd * Vector3.Dot(fwd, newCameraPos - oldCameraPos);
  138. // Remember the desired displacement for next frame
  139. var prev = extra.m_PreviousDisplacement;
  140. var displacement = newCameraPos - oldCameraPos;
  141. extra.m_PreviousDisplacement = displacement;
  142. if (!VirtualCamera.PreviousStateIsValid || deltaTime < 0 || m_Damping <= 0)
  143. extra.m_DampedDisplacement = Vector3.zero;
  144. else
  145. {
  146. // If a big change from previous frame's desired displacement is detected,
  147. // assume we are going around a corner and extract that difference for damping
  148. if (prev.sqrMagnitude > 0.01f && Vector2.Angle(prev, displacement) > k_cornerAngleTreshold)
  149. extra.m_DampedDisplacement += displacement - prev;
  150. extra.m_DampedDisplacement -= Damper.Damp(extra.m_DampedDisplacement, m_Damping, deltaTime);
  151. displacement -= extra.m_DampedDisplacement;
  152. }
  153. state.PositionCorrection += displacement;
  154. }
  155. }
  156. /// <summary>
  157. /// Calculates half frustum height for orthographic or perspective camera.
  158. /// For more info on frustum height, see <see cref="docs.unity3d.com/Manual/FrustumSizeAtDistance.html"/>
  159. /// </summary>
  160. /// <param name="state">CameraState for checking if Orthographic or Perspective</param>
  161. /// <param name="vcam">vcam, to check its position</param>
  162. /// <returns>Frustum height of the camera</returns>
  163. float CalculateHalfFrustumHeight(in CameraState state, in float cameraPosLocalZ)
  164. {
  165. float frustumHeight;
  166. if (state.Lens.Orthographic)
  167. {
  168. frustumHeight = state.Lens.OrthographicSize;
  169. }
  170. else
  171. {
  172. // distance between the collider's plane and the camera
  173. float distance = cameraPosLocalZ;
  174. frustumHeight = distance * Mathf.Tan(state.Lens.FieldOfView * 0.5f * Mathf.Deg2Rad);
  175. }
  176. return Mathf.Abs(frustumHeight);
  177. }
  178. class VcamExtraState
  179. {
  180. public Vector3 m_PreviousDisplacement;
  181. public Vector3 m_DampedDisplacement;
  182. public ConfinerOven.BakedSolution m_BakedSolution;
  183. public CinemachineVirtualCameraBase m_vcam;
  184. };
  185. ShapeCache m_shapeCache;
  186. /// <summary>
  187. /// ShapeCache: contains all states that dependent only on the settings in the confiner.
  188. /// </summary>
  189. struct ShapeCache
  190. {
  191. public ConfinerOven m_confinerOven;
  192. public List<List<Vector2>> m_OriginalPath; // in baked space, not including offset
  193. // These account for offset and transform change since baking
  194. public Matrix4x4 m_DeltaWorldToBaked;
  195. public Matrix4x4 m_DeltaBakedToWorld;
  196. float m_aspectRatio;
  197. float m_maxWindowSize;
  198. internal float m_maxComputationTimePerFrameInSeconds;
  199. Matrix4x4 m_bakedToWorld; // defines baked space
  200. Collider2D m_boundingShape2D;
  201. /// <summary>
  202. /// Invalidates shapeCache
  203. /// </summary>
  204. public void Invalidate()
  205. {
  206. m_aspectRatio = 0;
  207. m_maxWindowSize = -1;
  208. m_DeltaBakedToWorld = m_DeltaWorldToBaked = Matrix4x4.identity;
  209. m_boundingShape2D = null;
  210. m_OriginalPath = null;
  211. m_confinerOven = null;
  212. }
  213. /// <summary>
  214. /// Checks if we have a valid confiner state cache. Calculates cache if it is invalid (outdated or empty).
  215. /// </summary>
  216. /// <param name="boundingShape2D">Bounding shape</param>
  217. /// <param name="maxWindowSize">Max Window size</param>
  218. /// <param name="aspectRatio">Aspect ratio/param>
  219. /// <param name="confinerStateChanged">True, if the baked confiner state has changed.
  220. /// False, otherwise.</param>
  221. /// <returns>True, if input is valid. False, otherwise.</returns>
  222. public bool ValidateCache(
  223. Collider2D boundingShape2D, float maxWindowSize,
  224. float aspectRatio, out bool confinerStateChanged)
  225. {
  226. confinerStateChanged = false;
  227. if (IsValid(boundingShape2D, aspectRatio, maxWindowSize))
  228. {
  229. // Advance confiner baking
  230. if (m_confinerOven.State == ConfinerOven.BakingState.BAKING)
  231. {
  232. m_confinerOven.BakeConfiner(m_maxComputationTimePerFrameInSeconds);
  233. // If no longer baking, then confinerStateChanged
  234. confinerStateChanged = m_confinerOven.State != ConfinerOven.BakingState.BAKING;
  235. }
  236. // Update in case the polygon's transform changed
  237. CalculateDeltaTransformationMatrix();
  238. // If delta world to baked scale is uniform, cache is valid.
  239. Vector2 lossyScaleXY = m_DeltaWorldToBaked.lossyScale;
  240. if (lossyScaleXY.IsUniform())
  241. {
  242. return true;
  243. }
  244. }
  245. Invalidate();
  246. confinerStateChanged = true;
  247. Type colliderType = boundingShape2D == null ? null: boundingShape2D.GetType();
  248. if (colliderType == typeof(PolygonCollider2D))
  249. {
  250. var poly = boundingShape2D as PolygonCollider2D;
  251. m_OriginalPath = new List<List<Vector2>>();
  252. // Cache the current worldspace shape
  253. m_bakedToWorld = boundingShape2D.transform.localToWorldMatrix;
  254. for (int i = 0; i < poly.pathCount; ++i)
  255. {
  256. Vector2[] path = poly.GetPath(i);
  257. List<Vector2> dst = new List<Vector2>();
  258. for (int j = 0; j < path.Length; ++j)
  259. dst.Add(m_bakedToWorld.MultiplyPoint3x4(path[j]));
  260. m_OriginalPath.Add(dst);
  261. }
  262. }
  263. else if (colliderType == typeof(CompositeCollider2D))
  264. {
  265. var poly = boundingShape2D as CompositeCollider2D;
  266. m_OriginalPath = new List<List<Vector2>>();
  267. // Cache the current worldspace shape
  268. m_bakedToWorld = boundingShape2D.transform.localToWorldMatrix;
  269. var path = new Vector2[poly.pointCount];
  270. for (int i = 0; i < poly.pathCount; ++i)
  271. {
  272. int numPoints = poly.GetPath(i, path);
  273. List<Vector2> dst = new List<Vector2>();
  274. for (int j = 0; j < numPoints; ++j)
  275. dst.Add(m_bakedToWorld.MultiplyPoint3x4(path[j]));
  276. m_OriginalPath.Add(dst);
  277. }
  278. }
  279. else
  280. {
  281. return false; // input collider is invalid
  282. }
  283. m_confinerOven = new ConfinerOven(m_OriginalPath, aspectRatio, maxWindowSize);
  284. m_aspectRatio = aspectRatio;
  285. m_boundingShape2D = boundingShape2D;
  286. m_maxWindowSize = maxWindowSize;
  287. CalculateDeltaTransformationMatrix();
  288. return true;
  289. }
  290. bool IsValid(in Collider2D boundingShape2D, in float aspectRatio, in float maxOrthoSize)
  291. {
  292. return boundingShape2D != null && m_boundingShape2D != null &&
  293. m_boundingShape2D == boundingShape2D && // same boundingShape?
  294. m_OriginalPath != null && // first time?
  295. m_confinerOven != null && // cache not empty?
  296. Mathf.Abs(m_aspectRatio - aspectRatio) < UnityVectorExtensions.Epsilon && // aspect changed?
  297. Mathf.Abs(m_maxWindowSize - maxOrthoSize) < UnityVectorExtensions.Epsilon; // max ortho changed?
  298. }
  299. void CalculateDeltaTransformationMatrix()
  300. {
  301. // Account for current collider offset (in local space) and
  302. // incorporate the worldspace delta that the confiner has moved since baking
  303. var m = Matrix4x4.Translate(-m_boundingShape2D.offset) *
  304. m_boundingShape2D.transform.worldToLocalMatrix;
  305. m_DeltaWorldToBaked = m_bakedToWorld * m;
  306. m_DeltaBakedToWorld = m_DeltaWorldToBaked.inverse;
  307. }
  308. }
  309. #if UNITY_EDITOR
  310. // Used by editor gizmo drawer
  311. internal bool GetGizmoPaths(
  312. out List<List<Vector2>> originalPath,
  313. ref List<List<Vector2>> currentPath,
  314. out Matrix4x4 pathLocalToWorld)
  315. {
  316. originalPath = m_shapeCache.m_OriginalPath;
  317. pathLocalToWorld = m_shapeCache.m_DeltaBakedToWorld;
  318. currentPath.Clear();
  319. var allExtraStates = GetAllExtraStates<VcamExtraState>();
  320. for (var i = 0; i < allExtraStates.Count; ++i)
  321. {
  322. var e = allExtraStates[i];
  323. if (e.m_BakedSolution != null)
  324. {
  325. currentPath.AddRange(e.m_BakedSolution.GetBakedPath());
  326. }
  327. }
  328. return originalPath != null;
  329. }
  330. internal float BakeProgress() => m_shapeCache.m_confinerOven != null ? m_shapeCache.m_confinerOven.bakeProgress : 0f;
  331. internal bool ConfinerOvenTimedOut() => m_shapeCache.m_confinerOven != null &&
  332. m_shapeCache.m_confinerOven.State == ConfinerOven.BakingState.TIMEOUT;
  333. #endif
  334. void OnValidate()
  335. {
  336. m_Damping = Mathf.Max(0, m_Damping);
  337. m_shapeCache.m_maxComputationTimePerFrameInSeconds = m_MaxComputationTimePerFrameInSeconds;
  338. }
  339. void Reset()
  340. {
  341. m_Damping = 0.5f;
  342. m_MaxWindowSize = -1;
  343. }
  344. }
  345. #endif
  346. }