using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
///
/// This is a Cinemachine Component in the Body section of the component pipeline.
/// Its job is to position the camera in a fixed screen-space relationship to
/// the camera's Tracking target object, with offsets and damping.
///
/// The camera will be first moved along the camera Z axis until the target
/// is at the desired distance from the camera's X-Y plane. The camera will then
/// be moved in its XY plane until the target is at the desired point on
/// the camera's screen.
///
/// The Position Composer will only change the camera's position in space. It will not
/// re-orient or otherwise aim the camera.
///
/// For this component to work properly, the camera's tracking target must not be null.
/// The tracking target will define what the camera is looking at.
///
[AddComponentMenu("Cinemachine/Procedural/Position Control/Cinemachine Position Composer")]
[SaveDuringPlay]
[DisallowMultipleComponent]
[CameraPipeline(CinemachineCore.Stage.Body)]
[RequiredTarget(RequiredTargetAttribute.RequiredTargets.Tracking)]
[HelpURL(Documentation.BaseURL + "manual/CinemachinePositionComposer.html")]
public class CinemachinePositionComposer : CinemachineComponentBase
, CinemachineFreeLookModifier.IModifiablePositionDamping
, CinemachineFreeLookModifier.IModifiableDistance
, CinemachineFreeLookModifier.IModifiableComposition
{
/// The distance along the camera axis that will be maintained from the target
[Header("Camera Position")]
[Tooltip("The distance along the camera axis that will be maintained from the target")]
public float CameraDistance = 10f;
/// The camera will not move along its z-axis if the target is within
/// this distance of the specified camera distance
[Tooltip("The camera will not move along its z-axis if the target is within "
+ "this distance of the specified camera distance")]
public float DeadZoneDepth = 0;
/// 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;
///
/// Offset from the target object (in target-local co-ordinates). The camera will attempt to
/// frame the point which is the target's position plus this offset. Use it to correct for
/// cases when the target's origin is not the point of interest for the camera.
///
[Header("Target Tracking")]
[Tooltip("Offset from the target object (in target-local co-ordinates). "
+ "The camera will attempt to frame the point which is the target's position plus "
+ "this offset. Use it to correct for cases when the target's origin is not the "
+ "point of interest for the camera.")]
[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 Vector3 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;
const float kMinimumCameraDistance = 0.01f;
/// State information for damping
Vector3 m_PreviousCameraPosition = Vector3.zero;
internal PositionPredictor m_Predictor = new PositionPredictor(); // internal for tests
Quaternion m_prevRotation;
bool m_InheritingPosition;
void Reset()
{
TargetOffset = Vector3.zero;
Lookahead = new LookaheadSettings();
Damping = Vector3.one;
CameraDistance = 10;
Composition = ScreenComposerSettings.Default;
DeadZoneDepth = 0;
CenterOnActivate = true;
}
void OnValidate()
{
Damping.x = Mathf.Max(0, Damping.x);
Damping.y = Mathf.Max(0, Damping.y);
Damping.z = Mathf.Max(0, Damping.z);
CameraDistance = Mathf.Max(kMinimumCameraDistance, CameraDistance);
DeadZoneDepth = Mathf.Max(0, DeadZoneDepth);
Composition.Validate();
}
ScreenComposerSettings CinemachineFreeLookModifier.IModifiableComposition.Composition
{
get => Composition;
set => Composition = value;
}
Vector3 CinemachineFreeLookModifier.IModifiablePositionDamping.PositionDamping
{
get => Damping;
set => Damping = value;
}
float CinemachineFreeLookModifier.IModifiableDistance.Distance
{
get => CameraDistance;
set => CameraDistance = value;
}
/// True if component is enabled and has a valid Follow target
public override bool IsValid => enabled && FollowTarget != null;
/// Get the Cinemachine Pipeline stage that this component implements.
/// Always returns the Body stage
public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Body;
/// FramingTransposer algorithm takes camera orientation as input,
/// so even though it is a Body component, it must apply after Aim
public override bool BodyAppliesAfterAim => true;
/// Internal API for inspector
internal Vector3 TrackedPoint { get; private set; }
/// 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 == FollowTarget)
{
m_PreviousCameraPosition += 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_PreviousCameraPosition = pos;
m_prevRotation = rot;
}
///
/// Report maximum damping time needed for this component.
///
/// Highest damping setting in this component
public override float GetMaxDampTime() => Mathf.Max(Damping.x, Mathf.Max(Damping.y, Damping.z));
/// Notification that this virtual camera is going live.
/// Base class implementation does nothing.
/// The camera being deactivated. May be null.
/// Default world Up, set by the CinemachineBrain
/// Delta time for time-based effects (ignore if less than or equal to 0)
/// True if the vcam should do an internal update as a result of this call
public override bool OnTransitionFromCamera(
ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime)
{
if (fromCam != null
&& (VirtualCamera.State.BlendHint & CameraState.BlendHints.InheritPosition) != 0
&& !CinemachineCore.IsLiveInBlend(VirtualCamera))
{
m_PreviousCameraPosition = fromCam.State.RawPosition;
m_prevRotation = fromCam.State.RawOrientation;
m_InheritingPosition = true;
return true;
}
return false;
}
// Convert from screen coords to normalized orthographic distance coords
private Rect ScreenToOrtho(Rect rScreen, float orthoSize, float aspect)
{
var r = new Rect();
r.yMax = 2 * orthoSize * ((1f-rScreen.yMin) - 0.5f);
r.yMin = 2 * orthoSize * ((1f-rScreen.yMax) - 0.5f);
r.xMin = 2 * orthoSize * aspect * (rScreen.xMin - 0.5f);
r.xMax = 2 * orthoSize * aspect * (rScreen.xMax - 0.5f);
return r;
}
private Vector3 OrthoOffsetToScreenBounds(Vector3 targetPos2D, Rect screenRect)
{
// Bring it to the edge of screenRect, if outside. Leave it alone if inside.
var delta = Vector3.zero;
if (targetPos2D.x < screenRect.xMin)
delta.x += targetPos2D.x - screenRect.xMin;
if (targetPos2D.x > screenRect.xMax)
delta.x += targetPos2D.x - screenRect.xMax;
if (targetPos2D.y < screenRect.yMin)
delta.y += targetPos2D.y - screenRect.yMin;
if (targetPos2D.y > screenRect.yMax)
delta.y += targetPos2D.y - screenRect.yMax;
return delta;
}
/// Positions the virtual camera according to the transposer rules.
/// The current camera state
/// Used for damping. If less than 0, no damping is done.
public override void MutateCameraState(ref CameraState curState, float deltaTime)
{
var lens = curState.Lens;
var followTargetPosition = FollowTargetPosition + (FollowTargetRotation * TargetOffset);
bool previousStateIsValid = deltaTime >= 0 && VirtualCamera.PreviousStateIsValid;
if (!previousStateIsValid || VirtualCamera.FollowTargetChanged)
m_Predictor.Reset();
if (!previousStateIsValid)
{
m_PreviousCameraPosition = curState.RawPosition;
m_prevRotation = curState.RawOrientation;
if (!m_InheritingPosition && CenterOnActivate)
{
m_PreviousCameraPosition = FollowTargetPosition
+ (curState.RawOrientation * Vector3.back) * CameraDistance;
}
}
if (!IsValid)
{
m_InheritingPosition = false;
return;
}
var verticalFOV = lens.FieldOfView;
TrackedPoint = followTargetPosition;
if (Lookahead.Enabled && Lookahead.Time > Epsilon)
{
m_Predictor.Smoothing = Lookahead.Smoothing;
m_Predictor.AddPosition(followTargetPosition, deltaTime);
var delta = m_Predictor.PredictPositionDelta(Lookahead.Time);
if (Lookahead.IgnoreY)
delta = delta.ProjectOntoPlane(curState.ReferenceUp);
TrackedPoint = followTargetPosition + delta;
}
if (!curState.HasLookAt() || curState.ReferenceLookAt == FollowTargetPosition)
curState.ReferenceLookAt = followTargetPosition;
// Allow undamped camera orientation change
Quaternion localToWorld = curState.RawOrientation;
if (previousStateIsValid)
{
var q = localToWorld * Quaternion.Inverse(m_prevRotation);
m_PreviousCameraPosition = TrackedPoint + q * (m_PreviousCameraPosition - TrackedPoint);
}
m_prevRotation = localToWorld;
// Work in camera-local space
var camPosWorld = m_PreviousCameraPosition;
var worldToLocal = Quaternion.Inverse(localToWorld);
var cameraPos = worldToLocal * camPosWorld;
var targetPos = (worldToLocal * TrackedPoint) - cameraPos;
var lookAtPos = targetPos;
// Move along camera z
var cameraOffset = Vector3.zero;
float cameraMin = Mathf.Max(kMinimumCameraDistance, CameraDistance - DeadZoneDepth/2);
float cameraMax = Mathf.Max(cameraMin, CameraDistance + DeadZoneDepth/2);
float targetZ = Mathf.Min(targetPos.z, lookAtPos.z);
if (targetZ < cameraMin)
cameraOffset.z = targetZ - cameraMin;
if (targetZ > cameraMax)
cameraOffset.z = targetZ - cameraMax;
// Move along the XY plane
float screenSize = lens.Orthographic
? lens.OrthographicSize
: Mathf.Tan(0.5f * verticalFOV * Mathf.Deg2Rad) * (targetZ - cameraOffset.z);
var softGuideOrtho = ScreenToOrtho(Composition.DeadZoneRect, screenSize, lens.Aspect);
if (!previousStateIsValid)
{
// No damping or hard bounds, just snap to central bounds, skipping the soft zone
var rect = softGuideOrtho;
if (CenterOnActivate && !m_InheritingPosition)
rect = new Rect(rect.center, Vector2.zero); // Force to center
cameraOffset += OrthoOffsetToScreenBounds(targetPos, rect);
}
else
{
// Move it through the soft zone, with damping
cameraOffset += OrthoOffsetToScreenBounds(targetPos, softGuideOrtho);
cameraOffset = VirtualCamera.DetachedFollowTargetDamp(cameraOffset, Damping, deltaTime);
// Make sure the real target (not the lookahead one) is still in the frame
if (Composition.HardLimits.Enabled
&& (deltaTime < 0 || VirtualCamera.FollowTargetAttachment > 1 - Epsilon))
{
var hardGuideOrtho = ScreenToOrtho(Composition.HardLimitsRect, screenSize, lens.Aspect);
var realTargetPos = (worldToLocal * followTargetPosition) - cameraPos;
cameraOffset += OrthoOffsetToScreenBounds(
realTargetPos - cameraOffset, hardGuideOrtho);
}
}
curState.RawPosition = localToWorld * (cameraPos + cameraOffset);
m_PreviousCameraPosition = curState.RawPosition;
m_InheritingPosition = false;
}
}
}