Files
GameDevTVObstacleDodge/Library/PackageCache/com.unity.cinemachine@5342685532bb/Runtime/Components/CinemachineRotationComposer.cs

437 lines
21 KiB
C#

using UnityEngine;
using System;
using UnityEngine.Serialization;
namespace Unity.Cinemachine
{
/// <summary>
/// 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.
/// </summary>
[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
{
/// <summary>Settings for screen-space composition</summary>
[Header("Composition")]
[HideFoldout]
public ScreenComposerSettings Composition = ScreenComposerSettings.Default;
/// <summary>Force target to center of screen when this camera activates.
/// If false, will clamp target to the edges of the dead zone</summary>
[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;
/// <summary>Target offset from the object's origin in target-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 origin</summary>
[Header("Target Tracking")]
[Tooltip("Target offset from the target object's origin in target-local space. Use this to "
+ "fine-tune the tracking target position when the desired area is not the tracked object's origin.")]
[FormerlySerializedAs("TrackedObjectOffset")]
public Vector3 TargetOffset;
/// <summary>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.</summary>
[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;
/// <summary>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.</summary>
[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();
}
/// <summary>True if component is enabled and has a LookAt defined</summary>
public override bool IsValid => enabled && LookAtTarget != null;
/// <summary>Get the Cinemachine Pipeline stage that this component implements.
/// Always returns the Aim stage</summary>
public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Aim;
/// <summary>
/// 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.
/// </summary>
internal override bool CameraLooksAtTarget { get => true; }
/// <summary>Internal API for inspector</summary>
internal Vector3 TrackedPoint { get; private set; }
/// <summary>Internal API for inspector</summary>
internal ScreenComposerSettings GetEffectiveComposition => m_CompositionLastFrame;
/// <summary>Apply the target offsets to the target location.
/// Also set the TrackedPoint property, taking lookahead into account.</summary>
/// <param name="lookAt">The unoffset LookAt point</param>
/// <param name="up">Current effective world up</param>
/// <param name="deltaTime">Current effective deltaTime</param>
/// <returns>The LookAt point with the offset applied</returns>
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;
}
/// <summary>State information for damping</summary>
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
ScreenComposerSettings m_CompositionLastFrame;
/// <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="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 == LookAtTarget)
{
m_CameraPosPrevFrame += positionDelta;
m_LookAtPrevFrame += positionDelta;
m_Predictor.ApplyTransformDelta(positionDelta);
}
}
/// <summary>
/// Force the virtual camera to assume a given position and orientation
/// </summary>
/// <param name="pos">World-space position to take</param>
/// <param name="rot">World-space orientation to take</param>
public override void ForceCameraPosition(Vector3 pos, Quaternion rot)
{
base.ForceCameraPosition(pos, rot);
m_Predictor.ApplyRotationDelta(rot * Quaternion.Inverse(m_CameraOrientationPrevFrame));
m_CameraPosPrevFrame = pos;
m_CameraOrientationPrevFrame = rot;
}
/// <summary>
/// Report maximum damping time needed for this component.
/// </summary>
/// <returns>Highest damping setting in this component</returns>
public override float GetMaxDampTime() => Mathf.Max(Damping.x, Damping.y);
/// <summary>Sets the state's ReferenceLookAt, applying the offset.</summary>
/// <param name="curState">Input state that must be mutated</param>
/// <param name="deltaTime">Current effective deltaTime</param>
public override void PrePipelineMutateCameraState(ref CameraState curState, float deltaTime)
{
if (IsValid && curState.HasLookAt())
curState.ReferenceLookAt = GetLookAtPointAndSetTrackedPoint(
curState.ReferenceLookAt, curState.ReferenceUp, deltaTime);
}
/// <summary>Applies the composer rules and orients the camera accordingly</summary>
/// <param name="curState">The current camera state</param>
/// <param name="deltaTime">Used for calculating damping. If less than
/// zero, then target will snap to the center of the dead zone.</param>
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(ref 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, -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;
// Don't damp for change to desired screen position
if (Composition.ScreenPosition != m_CompositionLastFrame.ScreenPosition)
{
var p0 = m_Cache.DirectionFromScreen(m_CompositionLastFrame.ScreenPosition);
var p1 = m_Cache.DirectionFromScreen(Composition.ScreenPosition);
var q = Quaternion.identity.ApplyCameraRotation(m_ScreenOffsetPrevFrame, Vector3.up);
q = q * UnityVectorExtensions.SafeFromToRotation(p0, p1, Vector3.up);
m_ScreenOffsetPrevFrame = Quaternion.identity.GetCameraRotationToTarget(q * Vector3.forward, Vector3.up);
}
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, 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, -1);
}
m_CameraPosPrevFrame = curState.GetCorrectedPosition();
m_LookAtPrevFrame = TrackedPoint;
m_CameraOrientationPrevFrame = rigOrientation.normalized;
m_ScreenOffsetPrevFrame = m_CameraOrientationPrevFrame.GetCameraRotationToTarget(
m_LookAtPrevFrame - m_CameraPosPrevFrame, curState.ReferenceUp);
m_CompositionLastFrame = Composition;
curState.RawOrientation = m_CameraOrientationPrevFrame;
}
// Cache for some expensive calculations
struct FovCache
{
public Rect FovSoftGuideRect; // normalized
public Rect FovHardGuideRect; // normalized
public Vector2 Fov; // degrees
float m_OrthoSizeOverDistance;
float m_Aspect;
Rect m_DeadZoneRect;
Rect m_HardLimitRect;
Vector2 m_ScreenBounds; // at a distance of z=1
Vector2 m_HalfFovRad; // radians
public void UpdateCache(
ref LensSettings lens, Rect softGuide, Rect hardGuide, float targetDistance)
{
bool recalculate = m_Aspect != lens.Aspect
|| softGuide != m_DeadZoneRect || hardGuide != m_HardLimitRect
|| m_ScreenBounds == Vector2.zero;
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
m_HalfFovRad = new (Mathf.Atan(lens.Aspect * orthoOverDistance), Mathf.Atan(orthoOverDistance));
Fov = new (Mathf.Rad2Deg * 2 * m_HalfFovRad.x, Mathf.Rad2Deg * 2 * m_HalfFovRad.y);
m_OrthoSizeOverDistance = orthoOverDistance;
}
}
else
{
var verticalFOV = lens.FieldOfView;
if (Fov.y != verticalFOV)
recalculate = true;
if (recalculate)
{
var halfFovRad = verticalFOV * Mathf.Deg2Rad * 0.5f;
m_HalfFovRad = new ((float)Math.Atan(Math.Tan(halfFovRad) * lens.Aspect), halfFovRad);
Fov = new (Mathf.Rad2Deg * m_HalfFovRad.x * 2, verticalFOV);
m_OrthoSizeOverDistance = 0;
}
}
if (recalculate)
{
m_Aspect = lens.Aspect;
m_ScreenBounds = new Vector2(Mathf.Tan(m_HalfFovRad.x), Mathf.Tan(m_HalfFovRad.y));
m_DeadZoneRect = softGuide;
m_HardLimitRect = hardGuide;
FovSoftGuideRect = new Rect { min = ScreenToAngle(softGuide.min), max = ScreenToAngle(softGuide.max) };
FovHardGuideRect = new Rect { min = ScreenToAngle(hardGuide.min), max = ScreenToAngle(hardGuide.max) };
}
}
// Convert from normalized screen coords to normalized angular coords. Nonlinear relationship!
// Input: normalized screen point (0 == center, +-0.5 == edge)
// Input: normalized angle (0 == center, +-0.5 == edge)
readonly Vector2 ScreenToAngle(Vector2 p) => new (
(m_HalfFovRad.x + Mathf.Atan(2 * (p.x - 0.5f) * m_ScreenBounds.x)) / (2 * m_HalfFovRad.x),
(m_HalfFovRad.y + Mathf.Atan(2 * (p.y - 0.5f) * m_ScreenBounds.y)) / (2 * m_HalfFovRad.y));
// Input: normalized screen point (0 == center, +-0.5 == edge)
// Output: direction vector (not normalized) in camera-local space
public readonly Vector3 DirectionFromScreen(Vector2 p) =>
new (2 * (p.x - 0.5f) * m_ScreenBounds.x, -2 * (p.y - 0.5f) * m_ScreenBounds.y, 1);
}
FovCache m_Cache;
/// <summary>
/// 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.
/// </summary>
void RotateToScreenBounds(
ref CameraState state, Rect screenRect, Vector3 trackedPoint,
ref Quaternion rigOrientation, Vector2 fov, 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.y);
float min = (screenRect.yMin - 0.5f) * fov.y;
float max = (screenRect.yMax - 0.5f) * fov.y;
if (rotToRect.x < min)
rotToRect.x -= min;
else if (rotToRect.x > max)
rotToRect.x -= max;
else
rotToRect.x = 0;
min = (screenRect.xMin - 0.5f) * fov.x;
max = (screenRect.xMax - 0.5f) * fov.x;
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);
}
/// <summary>
/// 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.
/// </summary>
static 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;
}
}
}