#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