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;
}
}
}