#if CINEMACHINE_PHYSICS using System; using UnityEngine; namespace Unity.Cinemachine { /// /// An add-on module for CinemachineCamera that post-processes /// the final position of the camera. Based on the supplied settings, /// the Decollider will pull the camera out of any objects it is intersecting. /// [AddComponentMenu("Cinemachine/Procedural/Extensions/Cinemachine Decollider")] [SaveDuringPlay] [ExecuteAlways] [DisallowMultipleComponent] [HelpURL(Documentation.BaseURL + "manual/CinemachineDecollider.html")] public class CinemachineDecollider : CinemachineExtension { /// /// Camera will try to maintain this distance from any obstacle or terrain. /// [Tooltip("Camera will try to maintain this distance from any obstacle or terrain. Increase it " + "if necessary to keep the camera from clipping the near edge of obsacles.")] public float CameraRadius = 0.4f; /// Settings for pushing the camera out of intersecting objects [Serializable] public struct DecollisionSettings { /// When enabled, will attempt to push the camera out of intersecting objects [Tooltip("When enabled, will attempt to push the camera out of intersecting objects")] public bool Enabled; /// Objects on these layers will be detected. [Tooltip("Objects on these layers will be detected")] public LayerMask ObstacleLayers; /// Settings for resolving towards Follow target instead of LookAt. [Serializable] public struct FollowTargetSettings { /// Use the Follow target when resolving occlusions, instead of the LookAt target. [Tooltip("Use the Follow target when resolving occlusions, instead of the LookAt target.")] public bool Enabled; [Tooltip("Vertical offset from the Follow target's root, in target local space")] public float YOffset; } /// Use the Follow target when resolving occlusions, instead of the LookAt target. [EnabledProperty] public FollowTargetSettings UseFollowTarget; /// /// How gradually the camera returns to its normal position after having been corrected. /// Higher numbers will move the camera more gradually back to normal. /// [Range(0, 10)] [Tooltip("How gradually the camera returns to its normal position after having been corrected. " + "Higher numbers will move the camera more gradually back to normal.")] public float Damping; /// /// Smoothing to apply to obstruction resolution. Nearest camera point is held for at least this long. /// [Range(0, 2)] [Tooltip("Smoothing to apply to obstruction resolution. Nearest camera point is held for at least this long")] public float SmoothingTime; } /// When enabled, will attempt to push the camera out of intersecting objects [FoldoutWithEnabledButton] public DecollisionSettings Decollision; /// Settings for putting the camera on top of the terrain [Serializable] public struct TerrainSettings { /// When enabled, will attempt to place the camera on top of terrain layers [Tooltip("When enabled, will attempt to place the camera on top of terrain layers")] public bool Enabled; /// Colliders on these layers will be detected. [Tooltip("Colliders on these layers will be detected")] public LayerMask TerrainLayers; /// Specifies the maximum length of a raycast used to find terrain colliders. [Tooltip("Specifies the maximum length of a raycast used to find terrain colliders")] public float MaximumRaycast; /// /// How gradually the camera returns to its normal position after having been corrected. /// Higher numbers will move the camera more gradually back to normal. /// [Range(0, 10)] [Tooltip("How gradually the camera returns to its normal position after having been corrected. " + "Higher numbers will move the camera more gradually back to normal.")] public float Damping; } /// When enabled, will attempt to place the camera on top of terrain layers [FoldoutWithEnabledButton] public TerrainSettings TerrainResolution; static Collider[] s_ColliderBuffer = new Collider[10]; void OnValidate() { CameraRadius = Mathf.Max(0.01f, CameraRadius); } void Reset() { CameraRadius = 0.4f; TerrainResolution = new () { Enabled = true, TerrainLayers = 1, MaximumRaycast = 10, Damping = 0.5f }; Decollision = new () { Enabled = false, ObstacleLayers = 1, Damping = 0.5f }; } /// Cleanup protected override void OnDestroy() { RuntimeUtility.DestroyScratchCollider(); base.OnDestroy(); } /// /// Report maximum damping time needed for this component. /// /// Highest damping setting in this component public override float GetMaxDampTime() { return Mathf.Max( Decollision.Enabled ? Decollision.Damping : 0, TerrainResolution.Enabled ? TerrainResolution.Damping : 0); } /// Per-vcam extra state info class VcamExtraState : VcamExtraStateBase { public float PreviousTerrainDisplacement; public float PreviousObstacleDisplacement; public Vector3 PreviousCorrectedCameraPosition; float m_SmoothedDistance; float m_SmoothedTime; public float ApplyDistanceSmoothing(float distance, float smoothingTime) { if (m_SmoothedTime != 0 && smoothingTime > Epsilon) { if (CinemachineCore.CurrentTime - m_SmoothedTime < smoothingTime) return Mathf.Min(distance, m_SmoothedDistance); } return distance; } public void UpdateDistanceSmoothing(float distance) { if (m_SmoothedDistance == 0 || distance < m_SmoothedDistance) { m_SmoothedDistance = distance; m_SmoothedTime = CinemachineCore.CurrentTime; } } public void ResetDistanceSmoothing(float smoothingTime) { if (CinemachineCore.CurrentTime - m_SmoothedTime >= smoothingTime) m_SmoothedDistance = m_SmoothedTime = 0; } }; /// /// Callback to do the collision resolution and shot evaluation /// /// 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 extra = GetExtraState(vcam); var up = state.ReferenceUp; var initialCamPos = state.GetCorrectedPosition(); // Capture lookAt screen offset for composition preservation var hasLookAt = state.HasLookAt(); var lookAtPoint = hasLookAt ? state.ReferenceLookAt : state.GetCorrectedPosition(); var resolutionTargetPoint = GetAvoidanceResolutionTargetPoint(vcam, ref state); var lookAtScreenOffset = hasLookAt ? state.RawOrientation.GetCameraRotationToTarget( lookAtPoint - initialCamPos, state.ReferenceUp) : Vector2.zero; if (!vcam.PreviousStateIsValid) deltaTime = -1; // Resolve terrains extra.PreviousTerrainDisplacement = TerrainResolution.Enabled ? ResolveTerrain(extra, state.GetCorrectedPosition(), up, deltaTime) : 0; state.PositionCorrection += extra.PreviousTerrainDisplacement * up; // Resolve collisions if (Decollision.Enabled) { var oldCamPos = state.GetCorrectedPosition(); var displacement = DecollideCamera(oldCamPos, resolutionTargetPoint); displacement = ApplySmoothingAndDamping(displacement, resolutionTargetPoint, oldCamPos, extra, deltaTime); if (!displacement.AlmostZero()) { state.PositionCorrection += displacement; // Resolve terrains again, just in case the decollider messed it up. // No damping this time. var terrainDisplacement = TerrainResolution.Enabled ? ResolveTerrain(extra, state.GetCorrectedPosition(), up, -1) : 0; if (Mathf.Abs(terrainDisplacement) > Epsilon) { state.PositionCorrection += terrainDisplacement * up; extra.PreviousTerrainDisplacement = 0; } } } // Restore screen composition var newCamPos = state.GetCorrectedPosition(); if (hasLookAt && !(initialCamPos - newCamPos).AlmostZero()) { var q = Quaternion.LookRotation(lookAtPoint - newCamPos, up); state.RawOrientation = q.ApplyCameraRotation(-lookAtScreenOffset, up); if (deltaTime >= 0) { var dir0 = extra.PreviousCorrectedCameraPosition - lookAtPoint; var dir1 = newCamPos - lookAtPoint; if (dir0.sqrMagnitude > Epsilon && dir1.sqrMagnitude > Epsilon) state.RotationDampingBypass = UnityVectorExtensions.SafeFromToRotation(dir0, dir1, up); } } extra.PreviousCorrectedCameraPosition = newCamPos; } } Vector3 GetAvoidanceResolutionTargetPoint( CinemachineVirtualCameraBase vcam, ref CameraState state) { var resolutuionTargetPoint = state.HasLookAt() ? state.ReferenceLookAt : state.GetCorrectedPosition(); if (Decollision.UseFollowTarget.Enabled) { var target = vcam.Follow; if (target != null) { resolutuionTargetPoint = TargetPositionCache.GetTargetPosition(target) + TargetPositionCache.GetTargetRotation(target) * Vector3.up * Decollision.UseFollowTarget.YOffset; } } return resolutuionTargetPoint; } // Returns distance to move the camera in the up directon to stay on top of terrain float ResolveTerrain(VcamExtraState extra, Vector3 camPos, Vector3 up, float deltaTime) { float displacement = 0; if (RuntimeUtility.SphereCastIgnoreTag( new Ray(camPos + TerrainResolution.MaximumRaycast * up, -up), CameraRadius + Epsilon, out var hitInfo, TerrainResolution.MaximumRaycast, TerrainResolution.TerrainLayers, string.Empty)) { displacement = TerrainResolution.MaximumRaycast - hitInfo.distance + Epsilon; } // Apply damping if (deltaTime >= 0 && TerrainResolution.Damping > Epsilon) { if (displacement < extra.PreviousTerrainDisplacement) displacement = extra.PreviousTerrainDisplacement + Damper.Damp(displacement - extra.PreviousTerrainDisplacement, TerrainResolution.Damping, deltaTime); } return displacement; } Vector3 DecollideCamera(Vector3 cameraPos, Vector3 lookAtPoint) { // Don't handle layers already taken care of by terrain resolution var layers = Decollision.ObstacleLayers; if (TerrainResolution.Enabled) layers &= ~TerrainResolution.TerrainLayers; if (layers == 0) return Vector3.zero; // Detect any intersecting obstacles var dir = cameraPos - lookAtPoint; var capsuleLength = dir.magnitude; if (capsuleLength < Epsilon) return Vector3.zero; dir /= capsuleLength; capsuleLength = Mathf.Max(Epsilon, capsuleLength - CameraRadius * 2); lookAtPoint = cameraPos - dir * capsuleLength; Vector3 newCamPos = cameraPos; int numObstacles = Physics.OverlapCapsuleNonAlloc( lookAtPoint, cameraPos, CameraRadius - Epsilon, s_ColliderBuffer, Decollision.ObstacleLayers, QueryTriggerInteraction.Ignore); // Find the one that the camera is intersecting that is closest to the target if (numObstacles > 0) { var scratchCollider = RuntimeUtility.GetScratchCollider(); scratchCollider.radius = CameraRadius - Epsilon; float bestDistance = float.MaxValue; for (int i = 0; i < numObstacles; ++i) { var c = s_ColliderBuffer[i]; if (Physics.ComputePenetration( scratchCollider, newCamPos, Quaternion.identity, c, c.transform.position, c.transform.rotation, out var _, out var _)) { // Camera is intersecting - decollide in direction of lookAtPoint if (c.Raycast(new Ray(lookAtPoint, dir), out var hitInfo, capsuleLength)) { var distance = Mathf.Max(0, hitInfo.distance - CameraRadius); if (distance < bestDistance) { bestDistance = distance; newCamPos = lookAtPoint + dir * distance; } } } } } return newCamPos - cameraPos; } Vector3 ApplySmoothingAndDamping( Vector3 displacement, Vector3 lookAtPoint, Vector3 oldCamPos, VcamExtraState extra, float deltaTime) { var dir = oldCamPos + displacement - lookAtPoint; var distance = float.MaxValue;; if (deltaTime >= 0) { distance = dir.magnitude; if (distance > Epsilon) { // Apply smoothing dir /= distance; if (Decollision.SmoothingTime > Epsilon) { if (!displacement.AlmostZero()) extra.UpdateDistanceSmoothing(distance); distance = extra.ApplyDistanceSmoothing(distance, Decollision.SmoothingTime); displacement = (lookAtPoint + dir * distance) - oldCamPos; } if (displacement.AlmostZero()) { extra.ResetDistanceSmoothing(Decollision.SmoothingTime); // Apply damping if (Decollision.Damping > Epsilon) { if (distance > extra.PreviousObstacleDisplacement) { distance = extra.PreviousObstacleDisplacement + Damper.Damp(distance - extra.PreviousObstacleDisplacement, Decollision.Damping, deltaTime); displacement = (lookAtPoint + dir * distance) - oldCamPos; } } } } } extra.PreviousObstacleDisplacement = distance; return displacement; } } } #endif