using System; using UnityEngine; namespace Unity.Cinemachine.TargetTracking { /// /// The coordinate space to use when interpreting the offset from the target /// public enum BindingMode { /// /// Camera will be bound to the Follow target using a frame of reference consisting /// of the target's local frame at the moment when the virtual camera was enabled, /// or when the target was assigned. /// LockToTargetOnAssign = 0, /// /// Camera will be bound to the Follow target using a frame of reference consisting /// of the target's local frame, with the tilt and roll zeroed out. /// LockToTargetWithWorldUp = 1, /// /// Camera will be bound to the Follow target using a frame of reference consisting /// of the target's local frame, with the roll zeroed out. /// LockToTargetNoRoll = 2, /// /// Camera will be bound to the Follow target using the target's local frame. /// LockToTarget = 3, /// Camera will be bound to the Follow target using a world space offset. WorldSpace = 4, /// Offsets will be calculated relative to the target, using Camera-local axes LazyFollow = 5 } /// How to calculate the angular damping for the target orientation public enum AngularDampingMode { /// Use Euler angles to specify damping values. /// Subject to gimbal-lock when pitch is steep. Euler, /// /// Use quaternions to calculate angular damping. /// No per-channel control, but not susceptible to gimbal-lock Quaternion } /// /// Settings to control damping for target tracking. /// [Serializable] public struct TrackerSettings { /// The coordinate space to use when interpreting the offset from the target [Tooltip("The coordinate space to use when interpreting the offset from the target. This is also " + "used to set the camera's Up vector, which will be maintained when aiming the camera.")] public BindingMode BindingMode; /// How aggressively the camera tries to maintain the offset, per axis. /// Small numbers are more responsive, rapidly translating the camera to keep the target's /// offset. Larger numbers give a more heavy slowly responding camera. /// Using different settings per axis can yield a wide range of camera behaviors [Tooltip("How aggressively the camera tries to maintain the offset, per axis. Small numbers " + "are more responsive, rapidly translating the camera to keep the target's offset. " + "Larger numbers give a more heavy slowly responding camera. Using different settings per " + "axis can yield a wide range of camera behaviors.")] public Vector3 PositionDamping; /// How to calculate the angular damping for the target orientation. /// Use Quaternion if you expect the target to take on very steep pitches, which would /// be subject to gimbal lock if Eulers are used. public AngularDampingMode AngularDampingMode; /// How aggressively the camera tries to track the target's rotation, per axis. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera. [Tooltip("How aggressively the camera tries to track the target's rotation, per axis. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public Vector3 RotationDamping; /// How aggressively the camera tries to track the target's rotation. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera. [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to track the target's rotation. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public float QuaternionDamping; /// /// Get the default tracking settings /// public static TrackerSettings Default => new TrackerSettings { BindingMode = BindingMode.WorldSpace, PositionDamping = Vector3.one, AngularDampingMode = AngularDampingMode.Euler, RotationDamping = Vector3.one, QuaternionDamping = 1 }; /// /// Called from OnValidate(). Makes sure the settings are sensible. /// public void Validate() { PositionDamping.x = Mathf.Max(0, PositionDamping.x); PositionDamping.y = Mathf.Max(0, PositionDamping.y); PositionDamping.z = Mathf.Max(0, PositionDamping.z); RotationDamping.x = Mathf.Max(0, RotationDamping.x); RotationDamping.y = Mathf.Max(0, RotationDamping.y); RotationDamping.z = Mathf.Max(0, RotationDamping.z); QuaternionDamping = Mathf.Max(0, QuaternionDamping); } } /// /// Helpers for TrackerSettings /// public static class TrackerSettingsExtensions { /// /// Report maximum damping time needed for the current binding mode. /// /// The tracker settings /// Highest damping setting in this mode public static float GetMaxDampTime(this TrackerSettings s) { var d = s.GetEffectivePositionDamping(); var d2 = s.AngularDampingMode == AngularDampingMode.Euler ? s.GetEffectiveRotationDamping() : new Vector3(s.QuaternionDamping, 0, 0); var a = Mathf.Max(d.x, Mathf.Max(d.y, d.z)); var b = Mathf.Max(d2.x, Mathf.Max(d2.y, d2.z)); return Mathf.Max(a, b); } /// /// Get the effective position damping setting for current binding mode. /// For some binding modes, some axes are not damped. /// /// The tracker settings /// The damping settings applicable for this binding mode internal static Vector3 GetEffectivePositionDamping(this TrackerSettings s) { return s.BindingMode == BindingMode.LazyFollow ? new Vector3(0, s.PositionDamping.y, s.PositionDamping.z) : s.PositionDamping; } /// /// Get the effective rotation damping setting for current binding mode. /// For some binding modes, some axes are not damped. /// /// The tracker settings /// The damping settings applicable for this binding mode internal static Vector3 GetEffectiveRotationDamping(this TrackerSettings s) { switch (s.BindingMode) { case BindingMode.LockToTargetNoRoll: return new Vector3(s.RotationDamping.x, s.RotationDamping.y, 0); case BindingMode.LockToTargetWithWorldUp: return new Vector3(0, s.RotationDamping.y, 0); case BindingMode.WorldSpace: case BindingMode.LazyFollow: return Vector3.zero; default: return s.RotationDamping; } } } /// /// Helper object for implementing target following with damping /// struct Tracker { /// State information for damping public Vector3 PreviousTargetPosition { get; private set; } /// State information for damping public Quaternion PreviousReferenceOrientation { get; private set; } Quaternion m_TargetOrientationOnAssign; Vector3 m_PreviousOffset; Transform m_PreviousTarget; /// Initializes the state for previous frame if appropriate. /// The component caller /// Current effective deltaTime. /// Current binding mode for damping and offset /// Current effective world up direction. public void InitStateInfo( CinemachineComponentBase component, float deltaTime, BindingMode bindingMode, Vector3 up) { bool prevStateValid = deltaTime >= 0 && component.VirtualCamera.PreviousStateIsValid; if (m_PreviousTarget != component.FollowTarget || !prevStateValid) { m_PreviousTarget = component.FollowTarget; m_TargetOrientationOnAssign = component.FollowTargetRotation; } if (!prevStateValid) { PreviousTargetPosition = component.FollowTargetPosition; PreviousReferenceOrientation = GetReferenceOrientation(component, bindingMode, up); } } /// Internal API for the Inspector Editor, so it can draw a marker at the target /// The component caller /// Current binding mode for damping and offset /// Current effective world up /// The rotation of the Follow target, as understood by the Transposer. /// This is not necessarily the same thing as the actual target rotation public Quaternion GetReferenceOrientation( CinemachineComponentBase component, BindingMode bindingMode, Vector3 worldUp) { if (bindingMode == BindingMode.WorldSpace) return Quaternion.identity; if (component.FollowTarget != null) { Quaternion targetOrientation = component.FollowTarget.rotation; switch (bindingMode) { case BindingMode.LockToTargetOnAssign: return m_TargetOrientationOnAssign; case BindingMode.LockToTargetWithWorldUp: { Vector3 fwd = (targetOrientation * Vector3.forward).ProjectOntoPlane(worldUp); if (fwd.AlmostZero()) break; return Quaternion.LookRotation(fwd, worldUp); } case BindingMode.LockToTargetNoRoll: return Quaternion.LookRotation(targetOrientation * Vector3.forward, worldUp); case BindingMode.LockToTarget: return targetOrientation; case BindingMode.LazyFollow: { Vector3 fwd = (component.FollowTargetPosition - component.VcamState.RawPosition).ProjectOntoPlane(worldUp); if (fwd.AlmostZero()) break; return Quaternion.LookRotation(fwd, worldUp); } } } // Gimbal lock situation - use previous orientation if it exists return PreviousReferenceOrientation.normalized; } /// Positions the virtual camera according to the transposer rules. /// The component caller /// Used for damping. If less than 0, no damping is done. /// Current camera up /// Where we want to put the camera relative to the follow target /// Tracker settings /// Resulting camera position /// Damped target orientation public void TrackTarget( CinemachineComponentBase component, float deltaTime, Vector3 up, Vector3 desiredCameraOffset, in TrackerSettings settings, out Vector3 outTargetPosition, out Quaternion outTargetOrient) { var targetOrientation = GetReferenceOrientation(component, settings.BindingMode, up); var dampedOrientation = targetOrientation; bool prevStateValid = deltaTime >= 0 && component.VirtualCamera.PreviousStateIsValid; if (prevStateValid) { if (settings.AngularDampingMode == AngularDampingMode.Quaternion && settings.BindingMode == BindingMode.LockToTarget) { float t = component.VirtualCamera.DetachedFollowTargetDamp( 1, settings.QuaternionDamping, deltaTime); dampedOrientation = Quaternion.Slerp( PreviousReferenceOrientation, targetOrientation, t); } else if (settings.BindingMode != BindingMode.LazyFollow) { var relative = (Quaternion.Inverse(PreviousReferenceOrientation) * targetOrientation).eulerAngles; for (int i = 0; i < 3; ++i) { if (relative[i] > 180) relative[i] -= 360; if (Mathf.Abs(relative[i]) < 0.01f) // correct for precision drift relative[i] = 0; } relative = component.VirtualCamera.DetachedFollowTargetDamp( relative, settings.GetEffectiveRotationDamping(), deltaTime); dampedOrientation = PreviousReferenceOrientation * Quaternion.Euler(relative); } } PreviousReferenceOrientation = dampedOrientation; var targetPosition = component.FollowTargetPosition; var currentPosition = PreviousTargetPosition; var previousOffset = prevStateValid ? m_PreviousOffset : desiredCameraOffset; var offsetDelta = desiredCameraOffset - previousOffset; if (offsetDelta.sqrMagnitude > 0.01f) { var q = UnityVectorExtensions.SafeFromToRotation(m_PreviousOffset, desiredCameraOffset, up); currentPosition = targetPosition + q * (PreviousTargetPosition - targetPosition); } m_PreviousOffset = desiredCameraOffset; // Adjust for damping, which is done in camera-offset-local coords var positionDelta = targetPosition - currentPosition; if (prevStateValid) { Quaternion dampingSpace = desiredCameraOffset.AlmostZero() ? component.VcamState.RawOrientation : Quaternion.LookRotation(dampedOrientation * desiredCameraOffset, up); var localDelta = Quaternion.Inverse(dampingSpace) * positionDelta; localDelta = component.VirtualCamera.DetachedFollowTargetDamp( localDelta, settings.GetEffectivePositionDamping(), deltaTime); positionDelta = dampingSpace * localDelta; } currentPosition += positionDelta; outTargetPosition = PreviousTargetPosition = currentPosition; outTargetOrient = dampedOrientation; } /// Return a new damped target position that respects the minimum /// distance from the real target /// The component caller /// The effective position of the target, after damping /// Desired camera offset from target /// Current camera local +Z direction /// Effective world up /// The real undamped target position /// New camera offset, potentially adjusted to respect minimum distance from target public Vector3 GetOffsetForMinimumTargetDistance( CinemachineComponentBase component, Vector3 dampedTargetPos, Vector3 cameraOffset, Vector3 cameraFwd, Vector3 up, Vector3 actualTargetPos) { var posOffset = Vector3.zero; if (component.VirtualCamera.FollowTargetAttachment > 1 - UnityVectorExtensions.Epsilon) { cameraOffset = cameraOffset.ProjectOntoPlane(up); var minDistance = cameraOffset.magnitude * 0.2f; if (minDistance > 0) { actualTargetPos = actualTargetPos.ProjectOntoPlane(up); dampedTargetPos = dampedTargetPos.ProjectOntoPlane(up); var cameraPos = dampedTargetPos + cameraOffset; var d = Vector3.Dot( actualTargetPos - cameraPos, (dampedTargetPos - cameraPos).normalized); if (d < minDistance) { var dir = actualTargetPos - dampedTargetPos; var len = dir.magnitude; if (len < 0.01f) dir = -cameraFwd.ProjectOntoPlane(up); else dir /= len; posOffset = dir * (minDistance - d); } PreviousTargetPosition += posOffset; } } return posOffset; } /// This is called to notify the user that a target got warped, /// so that we can update its internal state to make the camera /// also warp seamlessly. /// The amount the target's position changed public void OnTargetObjectWarped(Vector3 positionDelta) { PreviousTargetPosition += positionDelta; } /// /// Force the virtual camera to assume a given position and orientation /// /// The component caller /// Current binding mode for damping and offset /// World-space position to take /// World-space orientation to take /// Camera offset from target in local space (relative to ReferenceOrientation) public void ForceCameraPosition( CinemachineComponentBase component, BindingMode bindingMode, Vector3 pos, Quaternion rot, Vector3 cameraOffsetLocalSpace) { // Infer target pos from camera var targetRot = bindingMode == BindingMode.LazyFollow ? rot : GetReferenceOrientation(component, bindingMode, component.VirtualCamera.State.ReferenceUp); PreviousTargetPosition = pos - targetRot * cameraOffsetLocalSpace; } } }