using System; using Cinemachine.Utility; using UnityEngine; namespace Cinemachine { /// <summary> /// This is a CinemachineComponent in the Body section of the component pipeline. /// Its job is to position the camera in a fixed relationship to the vcam's Follow /// target object, with offsets and damping. /// /// The Tansposer will only change the camera's position in space. It will not /// re-orient or otherwise aim the camera. To to that, you need to instruct /// the vcam in the Aim section of its pipeline. /// </summary> [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] [AddComponentMenu("")] // Don't display in add component menu [SaveDuringPlay] public class CinemachineTransposer : CinemachineComponentBase { /// <summary> /// The coordinate space to use when interpreting the offset from the target /// </summary> [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] public enum BindingMode { /// <summary> /// 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. /// </summary> LockToTargetOnAssign = 0, /// <summary> /// 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. /// </summary> LockToTargetWithWorldUp = 1, /// <summary> /// 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. /// </summary> LockToTargetNoRoll = 2, /// <summary> /// Camera will be bound to the Follow target using the target's local frame. /// </summary> LockToTarget = 3, /// <summary>Camera will be bound to the Follow target using a world space offset.</summary> WorldSpace = 4, /// <summary>Offsets will be calculated relative to the target, using Camera-local axes</summary> SimpleFollowWithWorldUp = 5 } /// <summary>The coordinate space to use when interpreting the offset from the target</summary> [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 m_BindingMode = BindingMode.LockToTargetWithWorldUp; /// <summary>The distance which the transposer will attempt to maintain from the transposer subject</summary> [Tooltip("The distance vector that the transposer will attempt to maintain from the Follow target")] public Vector3 m_FollowOffset = Vector3.back * 10f; /// <summary>How aggressively the camera tries to maintain the offset in the X-axis. /// Small numbers are more responsive, rapidly translating the camera to keep the target's /// x-axis offset. Larger numbers give a more heavy slowly responding camera. /// Using different settings per axis can yield a wide range of camera behaviors</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to maintain the offset in the X-axis. Small numbers " + "are more responsive, rapidly translating the camera to keep the target's x-axis offset. " + "Larger numbers give a more heavy slowly responding camera. Using different settings per " + "axis can yield a wide range of camera behaviors.")] public float m_XDamping = 1f; /// <summary>How aggressively the camera tries to maintain the offset in the Y-axis. /// Small numbers are more responsive, rapidly translating the camera to keep the target's /// y-axis offset. Larger numbers give a more heavy slowly responding camera. /// Using different settings per axis can yield a wide range of camera behaviors</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to maintain the offset in the Y-axis. Small numbers " + "are more responsive, rapidly translating the camera to keep the target's y-axis offset. " + "Larger numbers give a more heavy slowly responding camera. Using different settings per " + "axis can yield a wide range of camera behaviors.")] public float m_YDamping = 1f; /// <summary>How aggressively the camera tries to maintain the offset in the Z-axis. /// Small numbers are more responsive, rapidly translating the camera to keep the /// target's z-axis offset. Larger numbers give a more heavy slowly responding camera. /// Using different settings per axis can yield a wide range of camera behaviors</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to maintain the offset in the Z-axis. " + "Small numbers are more responsive, rapidly translating the camera to keep the " + "target's z-axis offset. Larger numbers give a more heavy slowly responding camera. " + "Using different settings per axis can yield a wide range of camera behaviors.")] public float m_ZDamping = 1f; /// <summary>How to calculate the angular damping for the target orientation</summary> public enum AngularDampingMode { /// <summary>Use Euler angles to specify damping values. /// Subject to gimbal-lock fwhen pitch is steep.</summary> Euler, /// <summary> /// Use quaternions to calculate angular damping. /// No per-channel control, but not susceptible to gimbal-lock</summary> Quaternion } /// <summary>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.</summary> public AngularDampingMode m_AngularDampingMode = AngularDampingMode.Euler; /// <summary>How aggressively the camera tries to track the target rotation's X angle. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to track the target rotation's X angle. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public float m_PitchDamping = 0; /// <summary>How aggressively the camera tries to track the target rotation's Y angle. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to track the target rotation's Y angle. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public float m_YawDamping = 0; /// <summary>How aggressively the camera tries to track the target rotation's Z angle. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to track the target rotation's Z angle. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public float m_RollDamping = 0f; /// <summary>How aggressively the camera tries to track the target's orientation. /// Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.</summary> [Range(0f, 20f)] [Tooltip("How aggressively the camera tries to track the target's orientation. " + "Small numbers are more responsive. Larger numbers give a more heavy slowly responding camera.")] public float m_AngularDamping = 0f; /// <summary>Derived classes should call this from their OnValidate() implementation</summary> protected virtual void OnValidate() { m_FollowOffset = EffectiveOffset; } /// <summary>Hide the offset in int inspector. Used by FreeLook.</summary> public bool HideOffsetInInspector { get; set; } /// <summary>Get the target offset, with sanitization</summary> public Vector3 EffectiveOffset { get { Vector3 offset = m_FollowOffset; if (m_BindingMode == BindingMode.SimpleFollowWithWorldUp) { offset.x = 0; offset.z = -Mathf.Abs(offset.z); } return offset; } } /// <summary>True if component is enabled and has a valid Follow target</summary> public override bool IsValid { get { return enabled && FollowTarget != null; } } /// <summary>Get the Cinemachine Pipeline stage that this component implements. /// Always returns the Body stage</summary> public override CinemachineCore.Stage Stage { get { return CinemachineCore.Stage.Body; } } /// <summary> /// Report maximum damping time needed for this component. /// </summary> /// <returns>Highest damping setting in this component</returns> public override float GetMaxDampTime() { var d = Damping; var d2 = AngularDamping; 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); } /// <summary>Positions the virtual camera according to the transposer rules.</summary> /// <param name="curState">The current camera state</param> /// <param name="deltaTime">Used for damping. If less than 0, no damping is done.</param> public override void MutateCameraState(ref CameraState curState, float deltaTime) { InitPrevFrameStateInfo(ref curState, deltaTime); if (IsValid) { Vector3 offset = EffectiveOffset; TrackTarget(deltaTime, curState.ReferenceUp, offset, out Vector3 pos, out Quaternion orient); offset = orient * offset; // Respect minimum target distance on XZ plane var targetPosition = FollowTargetPosition; pos += GetOffsetForMinimumTargetDistance( pos, offset, curState.RawOrientation * Vector3.forward, curState.ReferenceUp, targetPosition); curState.RawPosition = pos + offset; curState.ReferenceUp = orient * Vector3.up; } } /// <summary>This is called to notify the us that a target got warped, /// so that we can update its internal state to make the camera /// also warp seamlessy.</summary> /// <param name="target">The object that was warped</param> /// <param name="positionDelta">The amount the target's position changed</param> public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta) { base.OnTargetObjectWarped(target, positionDelta); if (target == FollowTarget) m_PreviousTargetPosition += positionDelta; } /// <summary> /// Force the virtual camera to assume a given position and orientation /// </summary> /// <param name="pos">Worldspace pposition to take</param> /// <param name="rot">Worldspace orientation to take</param> public override void ForceCameraPosition(Vector3 pos, Quaternion rot) { base.ForceCameraPosition(pos, rot); // Infer target pos from camera var targetRot = m_BindingMode == BindingMode.SimpleFollowWithWorldUp ? rot : GetReferenceOrientation(VirtualCamera.State.ReferenceUp); m_PreviousTargetPosition = pos - targetRot * EffectiveOffset; } /// <summary>Initializes the state for previous frame if appropriate.</summary> /// <param name="curState">The current camera state</param> /// <param name="deltaTime">Current effective deltaTime.</param> protected void InitPrevFrameStateInfo( ref CameraState curState, float deltaTime) { bool prevStateValid = deltaTime >= 0 && VirtualCamera.PreviousStateIsValid; if (m_previousTarget != FollowTarget || !prevStateValid) { m_previousTarget = FollowTarget; m_targetOrientationOnAssign = FollowTargetRotation; } if (!prevStateValid) { m_PreviousTargetPosition = FollowTargetPosition; m_PreviousReferenceOrientation = GetReferenceOrientation(curState.ReferenceUp); } } /// <summary>Positions the virtual camera according to the transposer rules.</summary> /// <param name="deltaTime">Used for damping. If less than 0, no damping is done.</param> /// <param name="up">Current camera up</param> /// <param name="desiredCameraOffset">Where we want to put the camera relative to the follow target</param> /// <param name="outTargetPosition">Resulting camera position</param> /// <param name="outTargetOrient">Damped target orientation</param> protected void TrackTarget( float deltaTime, Vector3 up, Vector3 desiredCameraOffset, out Vector3 outTargetPosition, out Quaternion outTargetOrient) { var targetOrientation = GetReferenceOrientation(up); var dampedOrientation = targetOrientation; bool prevStateValid = deltaTime >= 0 && VirtualCamera.PreviousStateIsValid; if (prevStateValid) { if (m_AngularDampingMode == AngularDampingMode.Quaternion && m_BindingMode == BindingMode.LockToTarget) { float t = VirtualCamera.DetachedFollowTargetDamp(1, m_AngularDamping, deltaTime); dampedOrientation = Quaternion.Slerp( m_PreviousReferenceOrientation, targetOrientation, t); } else { var relative = (Quaternion.Inverse(m_PreviousReferenceOrientation) * targetOrientation).eulerAngles; for (int i = 0; i < 3; ++i) { if (Mathf.Abs(relative[i]) < 0.01f) // correct for precision drift relative[i] = 0; else if (relative[i] > 180) relative[i] -= 360; } relative = VirtualCamera.DetachedFollowTargetDamp(relative, AngularDamping, deltaTime); dampedOrientation = m_PreviousReferenceOrientation * Quaternion.Euler(relative); } } m_PreviousReferenceOrientation = dampedOrientation; var targetPosition = FollowTargetPosition; var currentPosition = m_PreviousTargetPosition; var previousOffset = prevStateValid ? m_PreviousOffset : desiredCameraOffset; var offsetDelta = desiredCameraOffset - previousOffset; if (offsetDelta.sqrMagnitude > 0.01f) { var q = UnityVectorExtensions.SafeFromToRotation( m_PreviousOffset.ProjectOntoPlane(up), desiredCameraOffset.ProjectOntoPlane(up), up); currentPosition = targetPosition + q * (m_PreviousTargetPosition - targetPosition); } m_PreviousOffset = desiredCameraOffset; // Adjust for damping, which is done in camera-offset-local coords var positionDelta = targetPosition - currentPosition; if (prevStateValid) { Quaternion dampingSpace; if (desiredCameraOffset.AlmostZero()) dampingSpace = VcamState.RawOrientation; else dampingSpace = Quaternion.LookRotation(dampedOrientation * desiredCameraOffset, up); var localDelta = Quaternion.Inverse(dampingSpace) * positionDelta; localDelta = VirtualCamera.DetachedFollowTargetDamp(localDelta, Damping, deltaTime); positionDelta = dampingSpace * localDelta; } currentPosition += positionDelta; outTargetPosition = m_PreviousTargetPosition = currentPosition; outTargetOrient = dampedOrientation; } /// <summary>Return a new damped target position that respects the minimum /// distance from the real target</summary> /// <param name="dampedTargetPos">The effective position of the target, after damping</param> /// <param name="cameraOffset">Desired camera offset from target</param> /// <param name="cameraFwd">Current camera local +Z direction</param> /// <param name="up">Effective world up</param> /// <param name="actualTargetPos">The real undamped target position</param> /// <returns>New camera offset, potentially adjusted to respect minimum distance from target</returns> protected Vector3 GetOffsetForMinimumTargetDistance( Vector3 dampedTargetPos, Vector3 cameraOffset, Vector3 cameraFwd, Vector3 up, Vector3 actualTargetPos) { var posOffset = Vector3.zero; if (VirtualCamera.FollowTargetAttachment > 1 - 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); } m_PreviousTargetPosition += posOffset; } } return posOffset; } /// <summary> /// Damping speeds for each of the 3 axes of the offset from target /// </summary> protected Vector3 Damping { get { switch (m_BindingMode) { case BindingMode.SimpleFollowWithWorldUp: return new Vector3(0, m_YDamping, m_ZDamping); default: return new Vector3(m_XDamping, m_YDamping, m_ZDamping); } } } /// <summary> /// Damping speeds for each of the 3 axes of the target's rotation /// </summary> protected Vector3 AngularDamping { get { switch (m_BindingMode) { case BindingMode.LockToTargetNoRoll: return new Vector3(m_PitchDamping, m_YawDamping, 0); case BindingMode.LockToTargetWithWorldUp: return new Vector3(0, m_YawDamping, 0); case BindingMode.LockToTargetOnAssign: case BindingMode.WorldSpace: case BindingMode.SimpleFollowWithWorldUp: return Vector3.zero; default: return new Vector3(m_PitchDamping, m_YawDamping, m_RollDamping); } } } /// <summary>Internal API for the Inspector Editor, so it can draw a marker at the target</summary> /// <param name="worldUp">Current effective world up</param> /// <returns>The position of the Follow target</returns> public virtual Vector3 GetTargetCameraPosition(Vector3 worldUp) { if (!IsValid) return Vector3.zero; return FollowTargetPosition + GetReferenceOrientation(worldUp) * EffectiveOffset; } /// <summary>State information for damping</summary> Vector3 m_PreviousTargetPosition = Vector3.zero; Quaternion m_PreviousReferenceOrientation = Quaternion.identity; Quaternion m_targetOrientationOnAssign = Quaternion.identity; Vector3 m_PreviousOffset; Transform m_previousTarget = null; /// <summary>Internal API for the Inspector Editor, so it can draw a marker at the target</summary> /// <param name="worldUp">Current effective world up</param> /// <returns>The rotation of the Follow target, as understood by the Transposer. /// This is not necessarily the same thing as the actual target rotation</returns> public Quaternion GetReferenceOrientation(Vector3 worldUp) { if (m_BindingMode == BindingMode.WorldSpace) return Quaternion.identity; if (FollowTarget != null) { Quaternion targetOrientation = FollowTarget.rotation; switch (m_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.SimpleFollowWithWorldUp: { Vector3 fwd = (FollowTargetPosition - VcamState.RawPosition).ProjectOntoPlane(worldUp); if (fwd.AlmostZero()) break; return Quaternion.LookRotation(fwd, worldUp); } } } // Gimbal lock situation - use previous orientation if it exists #if UNITY_2019_1_OR_NEWER return m_PreviousReferenceOrientation.normalized; #else return m_PreviousReferenceOrientation.Normalized(); #endif } } }