Files

427 lines
21 KiB
C#
Raw Permalink Normal View History

2026-01-08 16:50:20 +00:00
using System;
using UnityEngine;
namespace Unity.Cinemachine.TargetTracking
{
/// <summary>
/// The coordinate space to use when interpreting the offset from the target
/// </summary>
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>
LazyFollow = 5
}
/// <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 when 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>
/// Settings to control damping for target tracking.
/// </summary>
[Serializable]
public struct TrackerSettings
{
/// <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 BindingMode;
/// <summary>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</summary>
[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;
/// <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 AngularDampingMode;
/// <summary>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.</summary>
[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;
/// <summary>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.</summary>
[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;
/// <summary>
/// Get the default tracking settings
/// </summary>
public static TrackerSettings Default => new TrackerSettings
{
BindingMode = BindingMode.WorldSpace,
PositionDamping = Vector3.one,
AngularDampingMode = AngularDampingMode.Euler,
RotationDamping = Vector3.one,
QuaternionDamping = 1
};
/// <summary>
/// Called from OnValidate(). Makes sure the settings are sensible.
/// </summary>
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);
}
}
/// <summary>
/// Helpers for TrackerSettings
/// </summary>
public static class TrackerSettingsExtensions
{
/// <summary>
/// Report maximum damping time needed for the current binding mode.
/// </summary>
/// <param name="s">The tracker settings</param>
/// <returns>Highest damping setting in this mode</returns>
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);
}
/// <summary>
/// Get the effective position damping setting for current binding mode.
/// For some binding modes, some axes are not damped.
/// </summary>
/// <param name="s">The tracker settings</param>
/// <returns>The damping settings applicable for this binding mode</returns>
internal static Vector3 GetEffectivePositionDamping(this TrackerSettings s)
{
return s.BindingMode == BindingMode.LazyFollow
? new Vector3(0, s.PositionDamping.y, s.PositionDamping.z) : s.PositionDamping;
}
/// <summary>
/// Get the effective rotation damping setting for current binding mode.
/// For some binding modes, some axes are not damped.
/// </summary>
/// <param name="s">The tracker settings</param>
/// <returns>The damping settings applicable for this binding mode</returns>
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;
}
}
}
/// <summary>
/// Helper object for implementing target following with damping
/// </summary>
struct Tracker
{
/// <summary>State information for damping</summary>
public Vector3 PreviousTargetPosition { get; private set; }
/// <summary>State information for damping</summary>
public Quaternion PreviousReferenceOrientation { get; private set; }
Vector3 m_PreviousOffset;
Vector3 m_PreviousTargetPositionDampingOffset;
Quaternion m_TargetOrientationOnAssign;
Transform m_PreviousTarget;
/// <summary>Initializes the state for previous frame if appropriate.</summary>
/// <param name="component">The component caller</param>
/// <param name="deltaTime">Current effective deltaTime.</param>
/// <param name="bindingMode">Current binding mode for damping and offset</param>
/// <param name="targetOffset">Offset from target root, in target-local space</param>
/// <param name="up">Current effective world up direction.</param>
public void InitStateInfo(
CinemachineComponentBase component, float deltaTime,
BindingMode bindingMode, Vector3 targetOffset, 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;
var state = component.VcamState;
PreviousReferenceOrientation = GetReferenceOrientation(component, bindingMode, targetOffset, up, ref state);
}
}
/// <summary>Internal API for the Inspector Editor, so it can draw a marker at the target</summary>
/// <param name="component">The component caller</param>
/// <param name="bindingMode">Current binding mode for damping and offset</param>
/// <param name="targetOffset">Offset from target root, in target-local space</param>
/// <param name="worldUp">Current effective world up</param>
/// <param name="cameraState">Calling camera's state - will not be modified</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 readonly Quaternion GetReferenceOrientation(
CinemachineComponentBase component,
BindingMode bindingMode, Vector3 targetOffset,
Vector3 worldUp, ref CameraState cameraState)
{
if (bindingMode == BindingMode.WorldSpace)
return Quaternion.identity;
if (component.FollowTarget != null)
{
var targetOrientation = component.FollowTargetRotation;
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:
{
var pos = component.FollowTargetPosition + component.FollowTargetRotation * targetOffset;
var fwd = (pos - cameraState.RawPosition).ProjectOntoPlane(worldUp);
if (fwd.AlmostZero())
break;
return Quaternion.LookRotation(fwd, worldUp);
}
}
}
// Gimbal lock situation - use previous orientation if it exists
if (PreviousReferenceOrientation == new Quaternion(0, 0, 0, 0))
return Quaternion.identity;
return PreviousReferenceOrientation.normalized;
}
/// <summary>Positions the virtual camera according to the transposer rules.</summary>
/// <param name="component">The component caller</param>
/// <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="settings">Tracker settings</param>
/// <param name="targetOffset">Offset from target root, in target-local space</param>
/// <param name="cameraState">Calling camera's state</param>
/// <param name="outTargetPosition">Resulting camera position</param>
/// <param name="outTargetOrient">Damped target orientation</param>
public void TrackTarget(
CinemachineComponentBase component,
float deltaTime, Vector3 up, Vector3 desiredCameraOffset,
in TrackerSettings settings, Vector3 targetOffset, ref CameraState cameraState,
out Vector3 outTargetPosition, out Quaternion outTargetOrient)
{
var vcam = component.VirtualCamera;
// Get the damped reference rotation
var referenceRot = GetReferenceOrientation(component, settings.BindingMode, targetOffset, up, ref cameraState);
var dampedRot = referenceRot;
bool prevStateValid = deltaTime >= 0 && vcam.PreviousStateIsValid;
if (prevStateValid && settings.BindingMode != BindingMode.LazyFollow && settings.BindingMode != BindingMode.WorldSpace)
{
if (settings.AngularDampingMode == AngularDampingMode.Quaternion
&& settings.BindingMode == BindingMode.LockToTarget)
{
float t = vcam.DetachedFollowTargetDamp(1, settings.QuaternionDamping, deltaTime);
dampedRot = Quaternion.Slerp(PreviousReferenceOrientation, referenceRot, t);
}
else
{
var relative = (Quaternion.Inverse(PreviousReferenceOrientation) * referenceRot).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 = vcam.DetachedFollowTargetDamp(relative, settings.GetEffectiveRotationDamping(), deltaTime);
dampedRot = PreviousReferenceOrientation * Quaternion.Euler(relative);
}
}
PreviousReferenceOrientation = dampedRot;
// Get the target position with the target offset applied
var targetPosition = GetTargetPositionWithOffset(component, settings.BindingMode, targetOffset, dampedRot);
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)
{
var dampingSpace = desiredCameraOffset.AlmostZero()
? vcam.State.RawOrientation : Quaternion.LookRotation(dampedRot * 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 = dampedRot;
m_PreviousTargetPositionDampingOffset = currentPosition - targetPosition;
}
Vector3 GetTargetPositionWithOffset(
CinemachineComponentBase component, BindingMode bindingMode, Vector3 targetOffset, Quaternion referenceOrient)
{
return component.FollowTargetPosition
+ (bindingMode == BindingMode.LazyFollow ? component.FollowTargetRotation : referenceOrient) * targetOffset;
}
/// <summary>Return a new damped target position that respects the minimum
/// distance from the real target</summary>
/// <param name="component">The component caller</param>
/// <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>
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;
}
/// <summary>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.</summary>
/// <param name="positionDelta">The amount the target's position changed</param>
public void OnTargetObjectWarped(Vector3 positionDelta)
{
PreviousTargetPosition += positionDelta;
}
/// <summary>
/// Force the virtual camera to assume a given position and orientation
/// </summary>
/// <param name="component">The component caller</param>
/// <param name="bindingMode">Current binding mode for damping and offset</param>
/// <param name="targetOffset">Offset from target root, in target-local space</param>
/// <param name="newState">Calling camera's new state</param>
public void OnForceCameraPosition(
CinemachineComponentBase component,
BindingMode bindingMode, Vector3 targetOffset,
ref CameraState newState)
{
var state = component.VcamState; // old state
var prevOrient = GetReferenceOrientation(
component, bindingMode, targetOffset, newState.ReferenceUp, ref state);
var targetPos = GetTargetPositionWithOffset(component, bindingMode, targetOffset, prevOrient);
var orient = GetReferenceOrientation(
component, bindingMode, targetOffset, newState.ReferenceUp, ref newState);
m_PreviousOffset = orient * (Quaternion.Inverse(prevOrient) * m_PreviousOffset);
PreviousReferenceOrientation = orient;
// Rotate the damping data also, to preserve position damping integrity
var deltaRot = newState.GetFinalOrientation() * Quaternion.Inverse(state.GetFinalOrientation());
m_PreviousTargetPositionDampingOffset = deltaRot * m_PreviousTargetPositionDampingOffset;
PreviousTargetPosition = targetPos + m_PreviousTargetPositionDampingOffset;
if (bindingMode == BindingMode.WorldSpace)
m_PreviousOffset = deltaRot * m_PreviousOffset;
}
}
}