using UnityEngine;
using Cinemachine.Utility;
namespace Cinemachine
{
///
/// This is a CinemachineComponent in the Aim section of the component pipeline.
/// Its job is to aim the camera at a target object, with configurable offsets, damping,
/// and composition rules.
///
/// In addition, if the target is a ICinemachineTargetGroup, the behaviour
/// will adjust the FOV and the camera distance to ensure that the entire group of targets
/// is framed properly.
///
[DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
[AddComponentMenu("")] // Don't display in add component menu
[SaveDuringPlay]
public class CinemachineGroupComposer : CinemachineComposer
{
/// How much of the screen to fill with the bounding box of the targets.
[Space]
[Tooltip("The bounding box of the targets should occupy this amount of the screen space. 1 means fill the whole screen. 0.5 means fill half the screen, etc.")]
public float m_GroupFramingSize = 0.8f;
/// What screen dimensions to consider when framing
[DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
public enum FramingMode
{
/// Consider only the horizontal dimension. Vertical framing is ignored.
Horizontal,
/// Consider only the vertical dimension. Horizontal framing is ignored.
Vertical,
/// The larger of the horizontal and vertical dimensions will dominate, to get the best fit.
HorizontalAndVertical
};
/// What screen dimensions to consider when framing
[Tooltip("What screen dimensions to consider when framing. Can be Horizontal, Vertical, or both")]
public FramingMode m_FramingMode = FramingMode.HorizontalAndVertical;
/// How aggressively the camera tries to frame the group.
/// Small numbers are more responsive
[Range(0, 20)]
[Tooltip("How aggressively the camera tries to frame the group. Small numbers are more responsive, rapidly adjusting the camera to keep the group in the frame. Larger numbers give a more heavy slowly responding camera.")]
public float m_FrameDamping = 2f;
/// How to adjust the camera to get the desired framing
public enum AdjustmentMode
{
/// Do not move the camera, only adjust the FOV.
ZoomOnly,
/// Just move the camera, don't change the FOV.
DollyOnly,
/// Move the camera as much as permitted by the ranges, then
/// adjust the FOV if necessary to make the shot.
DollyThenZoom
};
/// How to adjust the camera to get the desired framing
[Tooltip("How to adjust the camera to get the desired framing. You can zoom, dolly in/out, or do both.")]
public AdjustmentMode m_AdjustmentMode = AdjustmentMode.ZoomOnly;
/// How much closer to the target can the camera go?
[Tooltip("The maximum distance toward the target that this behaviour is allowed to move the camera.")]
public float m_MaxDollyIn = 5000f;
/// How much farther from the target can the camera go?
[Tooltip("The maximum distance away the target that this behaviour is allowed to move the camera.")]
public float m_MaxDollyOut = 5000f;
/// Set this to limit how close to the target the camera can get
[Tooltip("Set this to limit how close to the target the camera can get.")]
public float m_MinimumDistance = 1;
/// Set this to limit how far from the taregt the camera can get
[Tooltip("Set this to limit how far from the target the camera can get.")]
public float m_MaximumDistance = 5000f;
/// If adjusting FOV, will not set the FOV lower than this
[Range(1, 179)]
[Tooltip("If adjusting FOV, will not set the FOV lower than this.")]
public float m_MinimumFOV = 3;
/// If adjusting FOV, will not set the FOV higher than this
[Range(1, 179)]
[Tooltip("If adjusting FOV, will not set the FOV higher than this.")]
public float m_MaximumFOV = 60;
/// If adjusting Orthographic Size, will not set it lower than this
[Tooltip("If adjusting Orthographic Size, will not set it lower than this.")]
public float m_MinimumOrthoSize = 1;
/// If adjusting Orthographic Size, will not set it higher than this
[Tooltip("If adjusting Orthographic Size, will not set it higher than this.")]
public float m_MaximumOrthoSize = 5000;
private void OnValidate()
{
m_GroupFramingSize = Mathf.Max(0.001f, m_GroupFramingSize);
m_MaxDollyIn = Mathf.Max(0, m_MaxDollyIn);
m_MaxDollyOut = Mathf.Max(0, m_MaxDollyOut);
m_MinimumDistance = Mathf.Max(0, m_MinimumDistance);
m_MaximumDistance = Mathf.Max(m_MinimumDistance, m_MaximumDistance);
m_MinimumFOV = Mathf.Max(1, m_MinimumFOV);
m_MaximumFOV = Mathf.Clamp(m_MaximumFOV, m_MinimumFOV, 179);
m_MinimumOrthoSize = Mathf.Max(0.01f, m_MinimumOrthoSize);
m_MaximumOrthoSize = Mathf.Max(m_MinimumOrthoSize, m_MaximumOrthoSize);
}
// State for damping
float m_prevFramingDistance;
float m_prevFOV;
/// For editor visulaization of the calculated bounding box of the group
public Bounds LastBounds { get; private set; }
/// For editor visualization of the calculated bounding box of the group
public Matrix4x4 LastBoundsMatrix { get; private set; }
///
/// Report maximum damping time needed for this component.
///
/// Highest damping setting in this component
public override float GetMaxDampTime()
{
return Mathf.Max(base.GetMaxDampTime(), m_FrameDamping);
}
/// 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)
{
// Can't do anything without a group to look at
ICinemachineTargetGroup group = AbstractLookAtTargetGroup;
if (group == null)
{
base.MutateCameraState(ref curState, deltaTime);
return;
}
if (!IsValid || !curState.HasLookAt)
{
m_prevFramingDistance = 0;
m_prevFOV = 0;
return;
}
bool isOrthographic = curState.Lens.Orthographic;
bool canMoveCamera = !isOrthographic && m_AdjustmentMode != AdjustmentMode.ZoomOnly;
// Get the bounding box from camera's POV in view space
Vector3 up = curState.ReferenceUp;
var cameraPos = curState.RawPosition;
BoundingSphere s = group.Sphere;
Vector3 groupCenter = s.position;
Vector3 fwd = groupCenter - cameraPos;
float d = fwd.magnitude;
if (d < Epsilon)
return; // navel-gazing, get outa here
// Approximate looking at the group center
fwd /= d;
LastBoundsMatrix = Matrix4x4.TRS(
cameraPos, Quaternion.LookRotation(fwd, up), Vector3.one);
// Correction for the actual center
Bounds b;
if (isOrthographic)
{
b = group.GetViewSpaceBoundingBox(LastBoundsMatrix);
groupCenter = LastBoundsMatrix.MultiplyPoint3x4(b.center);
fwd = (groupCenter - cameraPos).normalized;
LastBoundsMatrix = Matrix4x4.TRS(cameraPos, Quaternion.LookRotation(fwd, up), Vector3.one);
b = group.GetViewSpaceBoundingBox(LastBoundsMatrix);
LastBounds = b;
}
else
{
b = GetScreenSpaceGroupBoundingBox(group, LastBoundsMatrix, out fwd);
LastBoundsMatrix = Matrix4x4.TRS(cameraPos, Quaternion.LookRotation(fwd, up), Vector3.one);
LastBounds = b;
groupCenter = cameraPos + fwd * b.center.z;
}
// Adjust bounds for framing size, and get target height
float boundsDepth = b.extents.z;
float targetHeight = GetTargetHeight(b.size / m_GroupFramingSize);
if (isOrthographic)
{
targetHeight = Mathf.Clamp(targetHeight / 2, m_MinimumOrthoSize, m_MaximumOrthoSize);
// ApplyDamping
if (deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
targetHeight = m_prevFOV + VirtualCamera.DetachedLookAtTargetDamp(
targetHeight - m_prevFOV, m_FrameDamping, deltaTime);
m_prevFOV = targetHeight;
LensSettings lens = curState.Lens;
lens.OrthographicSize = Mathf.Clamp(targetHeight, m_MinimumOrthoSize, m_MaximumOrthoSize);
curState.Lens = lens;
}
else
{
// Adjust height for perspective - we want the height at the near surface
float z = b.center.z;
if (z > boundsDepth)
targetHeight = Mathf.Lerp(0, targetHeight, (z - boundsDepth) / z);
// Move the camera
if (canMoveCamera)
{
// What distance from near edge would be needed to get the adjusted
// target height, at the current FOV
float targetDistance = boundsDepth
+ targetHeight / (2f * Mathf.Tan(curState.Lens.FieldOfView * Mathf.Deg2Rad / 2f));
// Clamp to respect min/max distance settings to the near surface of the bounds
targetDistance = Mathf.Clamp(
targetDistance, boundsDepth + m_MinimumDistance, boundsDepth + m_MaximumDistance);
// Clamp to respect min/max camera movement
float targetDelta = targetDistance - Vector3.Distance(curState.RawPosition, groupCenter);
targetDelta = Mathf.Clamp(targetDelta, -m_MaxDollyIn, m_MaxDollyOut);
// ApplyDamping
if (deltaTime >= 0 && VirtualCamera.PreviousStateIsValid)
{
float delta = targetDelta - m_prevFramingDistance;
delta = VirtualCamera.DetachedLookAtTargetDamp(delta, m_FrameDamping, deltaTime);
targetDelta = m_prevFramingDistance + delta;
}
m_prevFramingDistance = targetDelta;
curState.PositionCorrection -= fwd * targetDelta;
cameraPos -= fwd * targetDelta;
}
// Apply zoom
if (m_AdjustmentMode != AdjustmentMode.DollyOnly)
{
float nearBoundsDistance = (groupCenter - cameraPos).magnitude - boundsDepth;
float targetFOV = 179;
if (nearBoundsDistance > Epsilon)
targetFOV = 2f * Mathf.Atan(targetHeight / (2 * nearBoundsDistance)) * Mathf.Rad2Deg;
targetFOV = Mathf.Clamp(targetFOV, m_MinimumFOV, m_MaximumFOV);
// ApplyDamping
if (deltaTime >= 0 && m_prevFOV != 0 && VirtualCamera.PreviousStateIsValid)
targetFOV = m_prevFOV + VirtualCamera.DetachedLookAtTargetDamp(
targetFOV - m_prevFOV, m_FrameDamping, deltaTime);
m_prevFOV = targetFOV;
LensSettings lens = curState.Lens;
lens.FieldOfView = targetFOV;
curState.Lens = lens;
}
}
// Now compose normally
curState.ReferenceLookAt = GetLookAtPointAndSetTrackedPoint(
groupCenter, curState.ReferenceUp, deltaTime);
base.MutateCameraState(ref curState, deltaTime);
}
float GetTargetHeight(Vector2 boundsSize)
{
switch (m_FramingMode)
{
case FramingMode.Horizontal:
return Mathf.Max(Epsilon, boundsSize.x ) / VcamState.Lens.Aspect;
case FramingMode.Vertical:
return Mathf.Max(Epsilon, boundsSize.y);
default:
case FramingMode.HorizontalAndVertical:
return Mathf.Max(
Mathf.Max(Epsilon, boundsSize.x) / VcamState.Lens.Aspect,
Mathf.Max(Epsilon, boundsSize.y));
}
}
/// Point of view
/// New forward direction to use when interpreting the return value
/// Bounding box in a slightly rotated version of observer, as specified by newFwd
static Bounds GetScreenSpaceGroupBoundingBox(
ICinemachineTargetGroup group, Matrix4x4 observer, out Vector3 newFwd)
{
group.GetViewSpaceAngularBounds(observer, out var minAngles, out var maxAngles, out var zRange);
var shift = (minAngles + maxAngles) / 2;
newFwd = Quaternion.identity.ApplyCameraRotation(new Vector2(-shift.x, shift.y), Vector3.up) * Vector3.forward;
newFwd = observer.MultiplyVector(newFwd);
// For width and height (in camera space) of the bounding box, we use the values at the center of the box.
// This is an arbitrary choice. The gizmo drawer will take this into account when displaying
// the frustum bounds of the group
var d = zRange.y + zRange.x;
var angles = Vector2.Min(maxAngles - shift, new Vector2(89.5f, 89.5f)) * Mathf.Deg2Rad;
return new Bounds(
new Vector3(0, 0, d/2),
new Vector3(Mathf.Tan(angles.y) * d, Mathf.Tan(angles.x) * d, zRange.y - zRange.x));
}
}
}