using UnityEngine;
using System;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
///
/// This is a CinemachineComponent in the Aim section of the component pipeline.
/// Its job is to aim the camera at the vcam's LookAt target object, with
/// configurable offsets, damping, and composition rules.
///
/// The composer does not change the camera's position. It will only pan and tilt the
/// camera where it is, in order to get the desired framing. To move the camera, you have
/// to use the virtual camera's Body section.
///
[AddComponentMenu("Cinemachine/Procedural/Rotation Control/Cinemachine Rotation Composer")]
[SaveDuringPlay]
[DisallowMultipleComponent]
[CameraPipeline(CinemachineCore.Stage.Aim)]
[RequiredTarget(RequiredTargetAttribute.RequiredTargets.LookAt)]
[HelpURL(Documentation.BaseURL + "manual/CinemachineRotationComposer.html")]
public class CinemachineRotationComposer : CinemachineComponentBase,
CinemachineFreeLookModifier.IModifiableComposition
{
/// Settings for screen-space composition
[Header("Composition")]
[HideFoldout]
public ScreenComposerSettings Composition = ScreenComposerSettings.Default;
/// Force target to center of screen when this camera activates.
/// If false, will clamp target to the edges of the dead zone
[Tooltip("Force target to center of screen when this camera activates. If false, will "
+ "clamp target to the edges of the dead zone")]
public bool CenterOnActivate = true;
/// Target offset from the object's center in LOCAL space which
/// the Composer tracks. Use this to fine-tune the tracking target position
/// when the desired area is not in the tracked object's center
[Header("Target Tracking")]
[Tooltip("Target offset from the target object's center in target-local space. Use this to "
+ "fine-tune the tracking target position when the desired area is not the tracked object's center.")]
[FormerlySerializedAs("TrackedObjectOffset")]
public Vector3 TargetOffset;
/// How aggressively the camera tries to follow the target in screen space.
/// Small numbers are more responsive, rapidly orienting the camera to keep the target in
/// the dead zone. Larger numbers give a more heavy slowly responding camera.
/// Using different vertical and horizontal settings can yield a wide range of camera behaviors.
[Tooltip("How aggressively the camera tries to follow the target in the screen space. "
+ "Small numbers are more responsive, rapidly orienting the camera to keep the target in "
+ "the dead zone. Larger numbers give a more heavy slowly responding camera. Using different "
+ "vertical and horizontal settings can yield a wide range of camera behaviors.")]
public Vector2 Damping;
/// This setting will instruct the composer to adjust its target offset based
/// on the motion of the target. The composer will look at a point where it estimates
/// the target will be a little into the future.
[FoldoutWithEnabledButton]
public LookaheadSettings Lookahead;
void Reset()
{
TargetOffset = Vector3.zero;
Lookahead = new LookaheadSettings();
Damping = new Vector2(0.5f, 0.5f);
Composition = ScreenComposerSettings.Default;
CenterOnActivate = true;
}
void OnValidate()
{
Damping.x = Mathf.Max(0, Damping.x);
Damping.y = Mathf.Max(0, Damping.y);
Composition.Validate();
}
/// True if component is enabled and has a LookAt defined
public override bool IsValid => enabled && LookAtTarget != null;
/// Get the Cinemachine Pipeline stage that this component implements.
/// Always returns the Aim stage
public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Aim;
///
/// True if this component tries to make the camera look at the Tracking Target.
/// Used by inspector to warn the user of potential improper setup.
///
internal override bool CameraLooksAtTarget { get => true; }
/// Internal API for inspector
internal Vector3 TrackedPoint { get; private set; }
/// Apply the target offsets to the target location.
/// Also set the TrackedPoint property, taking lookahead into account.
/// The unoffset LookAt point
/// Current effective world up
/// Current effective deltaTime
/// The LookAt point with the offset applied
Vector3 GetLookAtPointAndSetTrackedPoint(
Vector3 lookAt, Vector3 up, float deltaTime)
{
var pos = lookAt;
if (LookAtTarget != null)
pos += LookAtTargetRotation * TargetOffset;
if (!Lookahead.Enabled || Lookahead.Time < Epsilon)
TrackedPoint = pos;
else
{
var resetLookahead = VirtualCamera.LookAtTargetChanged || !VirtualCamera.PreviousStateIsValid;
m_Predictor.Smoothing = Lookahead.Smoothing;
m_Predictor.AddPosition(pos, resetLookahead ? -1 : deltaTime);
var delta = m_Predictor.PredictPositionDelta(Lookahead.Time);
if (Lookahead.IgnoreY)
delta = delta.ProjectOntoPlane(up);
TrackedPoint = pos + delta;
}
return TrackedPoint;
}
/// State information for damping
Vector3 m_CameraPosPrevFrame = Vector3.zero;
Vector3 m_LookAtPrevFrame = Vector3.zero;
Vector2 m_ScreenOffsetPrevFrame = Vector2.zero;
Quaternion m_CameraOrientationPrevFrame = Quaternion.identity;
internal PositionPredictor m_Predictor = new PositionPredictor(); // internal for tests
/// 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 object that was warped
/// The amount the target's position changed
public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
{
base.OnTargetObjectWarped(target, positionDelta);
if (target == LookAtTarget)
{
m_CameraPosPrevFrame += positionDelta;
m_LookAtPrevFrame += positionDelta;
m_Predictor.ApplyTransformDelta(positionDelta);
}
}
///
/// Force the virtual camera to assume a given position and orientation
///
/// World-space position to take
/// World-space orientation to take
public override void ForceCameraPosition(Vector3 pos, Quaternion rot)
{
base.ForceCameraPosition(pos, rot);
m_CameraPosPrevFrame = pos;
m_CameraOrientationPrevFrame = rot;
}
///
/// Report maximum damping time needed for this component.
///
/// Highest damping setting in this component
public override float GetMaxDampTime() => Mathf.Max(Damping.x, Damping.y);
/// Sets the state's ReferenceLookAt, applying the offset.
/// Input state that must be mutated
/// Current effective deltaTime
public override void PrePipelineMutateCameraState(ref CameraState curState, float deltaTime)
{
if (IsValid && curState.HasLookAt())
curState.ReferenceLookAt = GetLookAtPointAndSetTrackedPoint(
curState.ReferenceLookAt, curState.ReferenceUp, deltaTime);
}
/// Applies the composer rules and orients the camera accordingly
/// The current camera state
/// Used for calculating damping. If less than
/// zero, then target will snap to the center of the dead zone.
public override void MutateCameraState(ref CameraState curState, float deltaTime)
{
if (!IsValid || !curState.HasLookAt())
return;
// Correct the tracked point in the event that it's behind the camera
// while the real target is in front
if (!(TrackedPoint - curState.ReferenceLookAt).AlmostZero())
{
var mid = Vector3.Lerp(curState.GetCorrectedPosition(), curState.ReferenceLookAt, 0.5f);
var toLookAt = curState.ReferenceLookAt - mid;
var toTracked = TrackedPoint - mid;
if (Vector3.Dot(toLookAt, toTracked) < 0)
{
float t = Vector3.Distance(curState.ReferenceLookAt, mid)
/ Vector3.Distance(curState.ReferenceLookAt, TrackedPoint);
TrackedPoint = Vector3.Lerp(curState.ReferenceLookAt, TrackedPoint, t);
}
}
float targetDistance = (TrackedPoint - curState.GetCorrectedPosition()).magnitude;
if (targetDistance < Epsilon)
{
if (deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
curState.RawOrientation = m_CameraOrientationPrevFrame;
return; // navel-gazing, get outa here
}
// Expensive FOV calculations
m_Cache.UpdateCache(curState.Lens, Composition.DeadZoneRect, Composition.HardLimitsRect, targetDistance);
Quaternion rigOrientation = curState.RawOrientation;
if (deltaTime < 0 || !VirtualCamera.PreviousStateIsValid)
{
// No damping, just snap to central bounds, skipping the soft zone
rigOrientation = Quaternion.LookRotation(
rigOrientation * Vector3.forward, curState.ReferenceUp);
var rect = m_Cache.FovSoftGuideRect;
if (CenterOnActivate)
rect = new Rect(rect.center, Vector2.zero); // Force to center
RotateToScreenBounds(
ref curState, rect, curState.ReferenceLookAt,
ref rigOrientation, m_Cache.Fov, m_Cache.FovH, -1);
}
else
{
// Start with previous frame's orientation (but with current up)
var dir = m_LookAtPrevFrame - m_CameraPosPrevFrame;
if (dir.AlmostZero())
rigOrientation = Quaternion.LookRotation(
m_CameraOrientationPrevFrame * Vector3.forward, curState.ReferenceUp);
else
{
dir = curState.RotationDampingBypass * dir;
rigOrientation = Quaternion.LookRotation(dir, curState.ReferenceUp);
rigOrientation = rigOrientation.ApplyCameraRotation(
-m_ScreenOffsetPrevFrame, curState.ReferenceUp);
}
// Move target through the soft zone, with damping
RotateToScreenBounds(
ref curState, m_Cache.FovSoftGuideRect, TrackedPoint,
ref rigOrientation, m_Cache.Fov, m_Cache.FovH, deltaTime);
// Force the actual target (not the lookahead one) into the hard bounds, no damping
if (Composition.HardLimits.Enabled && (deltaTime < 0 || VirtualCamera.LookAtTargetAttachment > 1 - Epsilon))
RotateToScreenBounds(
ref curState, m_Cache.FovHardGuideRect, curState.ReferenceLookAt,
ref rigOrientation, m_Cache.Fov, m_Cache.FovH, -1);
}
m_CameraPosPrevFrame = curState.GetCorrectedPosition();
m_LookAtPrevFrame = TrackedPoint;
m_CameraOrientationPrevFrame = rigOrientation.normalized;
m_ScreenOffsetPrevFrame = m_CameraOrientationPrevFrame.GetCameraRotationToTarget(
m_LookAtPrevFrame - curState.GetCorrectedPosition(), curState.ReferenceUp);
curState.RawOrientation = m_CameraOrientationPrevFrame;
}
// Cache for some expensive calculations
struct FovCache
{
public Rect FovSoftGuideRect;
public Rect FovHardGuideRect;
public float FovH;
public float Fov;
float m_OrthoSizeOverDistance;
float m_Aspect;
Rect m_DeadZoneRect;
Rect m_HardLimitRect;
public void UpdateCache(
LensSettings lens, Rect softGuide, Rect hardGuide, float targetDistance)
{
bool recalculate = m_Aspect != lens.Aspect
|| softGuide != m_DeadZoneRect || hardGuide != m_HardLimitRect;
if (lens.Orthographic)
{
float orthoOverDistance = Mathf.Abs(lens.OrthographicSize / targetDistance);
if (m_OrthoSizeOverDistance == 0
|| Mathf.Abs(orthoOverDistance - m_OrthoSizeOverDistance) / m_OrthoSizeOverDistance
> m_OrthoSizeOverDistance * 0.01f)
recalculate = true;
if (recalculate)
{
// Calculate effective fov - fake it for ortho based on target distance
Fov = Mathf.Rad2Deg * 2 * Mathf.Atan(orthoOverDistance);
FovH = Mathf.Rad2Deg * 2 * Mathf.Atan(lens.Aspect * orthoOverDistance);
m_OrthoSizeOverDistance = orthoOverDistance;
}
}
else
{
var verticalFOV = lens.FieldOfView;
if (Fov != verticalFOV)
recalculate = true;
if (recalculate)
{
Fov = verticalFOV;
double radHFOV = 2 * Math.Atan(Math.Tan(Fov * Mathf.Deg2Rad / 2) * lens.Aspect);
FovH = (float)(Mathf.Rad2Deg * radHFOV);
m_OrthoSizeOverDistance = 0;
}
}
if (recalculate)
{
FovSoftGuideRect = ScreenToFOV(softGuide, Fov, FovH, lens.Aspect);
m_DeadZoneRect = softGuide;
FovHardGuideRect = ScreenToFOV(hardGuide, Fov, FovH, lens.Aspect);
m_HardLimitRect = hardGuide;
m_Aspect = lens.Aspect;
}
}
// Convert from screen coords to normalized FOV angular coords
static Rect ScreenToFOV(Rect rScreen, float fov, float fovH, float aspect)
{
var r = new Rect(rScreen);
var persp = Matrix4x4.Perspective(fov, aspect, 0.0001f, 2f).inverse;
var p = persp.MultiplyPoint(new Vector3(0, (r.yMin * 2f) - 1f, 0.5f)); p.z = -p.z;
float angle = UnityVectorExtensions.SignedAngle(Vector3.forward, p, Vector3.left);
r.yMin = ((fov / 2) + angle) / fov;
p = persp.MultiplyPoint(new Vector3(0, (r.yMax * 2f) - 1f, 0.5f)); p.z = -p.z;
angle = UnityVectorExtensions.SignedAngle(Vector3.forward, p, Vector3.left);
r.yMax = ((fov / 2) + angle) / fov;
p = persp.MultiplyPoint(new Vector3((r.xMin * 2f) - 1f, 0, 0.5f)); p.z = -p.z;
angle = UnityVectorExtensions.SignedAngle(Vector3.forward, p, Vector3.up);
r.xMin = ((fovH / 2) + angle) / fovH;
p = persp.MultiplyPoint(new Vector3((r.xMax * 2f) - 1f, 0, 0.5f)); p.z = -p.z;
angle = UnityVectorExtensions.SignedAngle(Vector3.forward, p, Vector3.up);
r.xMax = ((fovH / 2) + angle) / fovH;
return r;
}
}
FovCache m_Cache;
///
/// Adjust the rigOrientation to put the camera within the screen bounds.
/// If deltaTime >= 0 then damping will be applied.
/// Assumes that currentOrientation fwd is such that input rigOrientation's
/// local up is NEVER NEVER NEVER pointing downwards, relative to
/// state.ReferenceUp. If this condition is violated
/// then you will see crazy spinning. That's the symptom.
///
void RotateToScreenBounds(
ref CameraState state, Rect screenRect, Vector3 trackedPoint,
ref Quaternion rigOrientation, float fov, float fovH, float deltaTime)
{
var targetDir = trackedPoint - state.GetCorrectedPosition();
var rotToRect = rigOrientation.GetCameraRotationToTarget(targetDir, state.ReferenceUp);
// Bring it to the edge of screenRect, if outside. Leave it alone if inside.
ClampVerticalBounds(ref screenRect, targetDir, state.ReferenceUp, fov);
float min = (screenRect.yMin - 0.5f) * fov;
float max = (screenRect.yMax - 0.5f) * fov;
if (rotToRect.x < min)
rotToRect.x -= min;
else if (rotToRect.x > max)
rotToRect.x -= max;
else
rotToRect.x = 0;
min = (screenRect.xMin - 0.5f) * fovH;
max = (screenRect.xMax - 0.5f) * fovH;
if (rotToRect.y < min)
rotToRect.y -= min;
else if (rotToRect.y > max)
rotToRect.y -= max;
else
rotToRect.y = 0;
// Apply damping
if (deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
{
rotToRect.x = VirtualCamera.DetachedLookAtTargetDamp(
rotToRect.x, Damping.y, deltaTime);
rotToRect.y = VirtualCamera.DetachedLookAtTargetDamp(
rotToRect.y, Damping.x, deltaTime);
}
// Rotate
rigOrientation = rigOrientation.ApplyCameraRotation(rotToRect, state.ReferenceUp);
}
///
/// Prevent upside-down camera situation. This can happen if we have a high
/// camera pitch combined with composer settings that cause the camera to tilt
/// beyond the vertical in order to produce the desired framing. We prevent this by
/// clamping the composer's vertical settings so that this situation can't happen.
///
bool ClampVerticalBounds(ref Rect r, Vector3 dir, Vector3 up, float fov)
{
float angle = UnityVectorExtensions.Angle(dir, up);
float halfFov = (fov / 2f) + 1; // give it a little extra to accommodate precision errors
if (angle < halfFov)
{
// looking up
float maxY = 1f - (halfFov - angle) / fov;
if (r.yMax > maxY)
{
r.yMin = Mathf.Min(r.yMin, maxY);
r.yMax = Mathf.Min(r.yMax, maxY);
return true;
}
}
if (angle > (180 - halfFov))
{
// looking down
float minY = (angle - (180 - halfFov)) / fov;
if (minY > r.yMin)
{
r.yMin = Mathf.Max(r.yMin, minY);
r.yMax = Mathf.Max(r.yMax, minY);
return true;
}
}
return false;
}
ScreenComposerSettings CinemachineFreeLookModifier.IModifiableComposition.Composition
{
get => Composition;
set => Composition = value;
}
}
}