using System; using System.Collections.Generic; using UnityEngine; namespace Unity.Cinemachine { /// /// This is a CinemachineComponent in the Aim section of the component pipeline. /// Its job is to aim the camera in response to the user's mouse or joystick input. /// /// This component does not change the camera's position. /// [AddComponentMenu("Cinemachine/Procedural/Rotation Control/Cinemachine Pan Tilt")] [SaveDuringPlay] [DisallowMultipleComponent] [CameraPipeline(CinemachineCore.Stage.Aim)] [HelpURL(Documentation.BaseURL + "manual/CinemachinePanTilt.html")] public class CinemachinePanTilt : CinemachineComponentBase, IInputAxisOwner, IInputAxisResetSource , CinemachineFreeLookModifier.IModifierValueSource { /// Defines the reference frame against which pan and tilt rotations are made. public enum ReferenceFrames { /// Pan and tilt are relative to parent object's local axes, /// or World axes if no parent object ParentObject, /// Pan and tilt angles are relative to world axes World, /// Pan and tilt are relative to Tracking target's local axes TrackingTarget, /// Pan and tilt are relative to LookAt target's local axes LookAtTarget } /// Defines the reference frame against which pan and tilt rotations are made. public ReferenceFrames ReferenceFrame = ReferenceFrames.ParentObject; /// Axis representing the current horizontal rotation. Value is in degrees /// and represents a rotation about the up vector [Tooltip("Axis representing the current horizontal rotation. Value is in degrees " + "and represents a rotation about the Y axis.")] public InputAxis PanAxis = DefaultPan; /// Axis representing the current vertical rotation. Value is in degrees /// and represents a rotation about the right vector [Tooltip("Axis representing the current vertical rotation. Value is in degrees " + "and represents a rotation about the X axis.")] public InputAxis TiltAxis = DefaultTilt; Quaternion m_PreviousCameraRotation; /// /// Input axis controller registers here a delegate to call when the camera is reset /// Action m_ResetHandler; void OnValidate() { PanAxis.Validate(); TiltAxis.Range.x = Mathf.Clamp(TiltAxis.Range.x, -90, 90); TiltAxis.Range.y = Mathf.Clamp(TiltAxis.Range.y, -90, 90); TiltAxis.Validate(); } void Reset() { PanAxis = DefaultPan; TiltAxis = DefaultTilt; ReferenceFrame = ReferenceFrames.ParentObject; } static InputAxis DefaultPan => new () { Value = 0, Range = new Vector2(-180, 180), Wrap = true, Center = 0, Recentering = InputAxis.RecenteringSettings.Default }; static InputAxis DefaultTilt => new () { Value = 0, Range = new Vector2(-70, 70), Wrap = false, Center = 0, Recentering = InputAxis.RecenteringSettings.Default }; /// Report the available input axes /// Output list to which the axes will be added void IInputAxisOwner.GetInputAxes(List axes) { axes.Add(new () { DrivenAxis = () => ref PanAxis, Name = "Look X (Pan)", Hint = IInputAxisOwner.AxisDescriptor.Hints.X }); axes.Add(new () { DrivenAxis = () => ref TiltAxis, Name = "Look Y (Tilt)", Hint = IInputAxisOwner.AxisDescriptor.Hints.Y }); } /// Register a handler that will be called when input needs to be reset /// The handler to register void IInputAxisResetSource.RegisterResetHandler(Action handler) => m_ResetHandler += handler; /// Unregister a handler that will be called when input needs to be reset /// The handler to unregister void IInputAxisResetSource.UnregisterResetHandler(Action handler) => m_ResetHandler -= handler; float CinemachineFreeLookModifier.IModifierValueSource.NormalizedModifierValue { get { var r = TiltAxis.Range.y - TiltAxis.Range.x; return (TiltAxis.Value - TiltAxis.Range.x) / (r > 0.001f ? r : 1) * 2 - 1; } } /// Inspector checks this and displays warning if no handler bool IInputAxisResetSource.HasResetHandler => m_ResetHandler != null; /// True if component is enabled and has a LookAt defined public override bool IsValid => enabled; /// Get the Cinemachine Pipeline stage that this component implements. /// Always returns the Aim stage public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Aim; /// Does nothing /// /// public override void PrePipelineMutateCameraState(ref CameraState state, float deltaTime) {} /// Applies the axis values and orients the camera accordingly /// The current camera state /// Used for calculating damping. Not used. public override void MutateCameraState(ref CameraState curState, float deltaTime) { if (!IsValid) return; if (deltaTime < 0 || !VirtualCamera.PreviousStateIsValid || !CinemachineCore.IsLive(VirtualCamera)) m_ResetHandler?.Invoke(); var referenceFrame = GetReferenceFrame(curState.ReferenceUp); var rot = referenceFrame * Quaternion.Euler(TiltAxis.Value, PanAxis.Value, 0); curState.RawOrientation = rot; if (VirtualCamera.PreviousStateIsValid) curState.RotationDampingBypass = curState.RotationDampingBypass * UnityVectorExtensions.SafeFromToRotation( m_PreviousCameraRotation * Vector3.forward, rot * Vector3.forward, curState.ReferenceUp); m_PreviousCameraRotation = rot; var gotInputX = PanAxis.TrackValueChange(); var gotInputY = TiltAxis.TrackValueChange(); // Sync recentering if the recenter times match if (PanAxis.Recentering.Time == TiltAxis.Recentering.Time) { gotInputX |= gotInputY; gotInputY |= gotInputX; } PanAxis.UpdateRecentering(deltaTime, gotInputX); TiltAxis.UpdateRecentering(deltaTime, gotInputY); } /// /// Force the virtual camera to assume a given position and orientation. /// Procedural placement then takes over /// /// World-space position to take /// World-space orientation to take public override void ForceCameraPosition(Vector3 pos, Quaternion rot) => SetAxesForRotation(rot); /// 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) { m_ResetHandler?.Invoke(); // Cancel re-centering if (fromCam != null && (VirtualCamera.State.BlendHint & CameraState.BlendHints.InheritPosition) != 0 && !CinemachineCore.IsLiveInBlend(VirtualCamera)) { SetAxesForRotation(fromCam.State.RawOrientation); return true; } return false; } void SetAxesForRotation(Quaternion targetRot) { m_ResetHandler?.Invoke(); // cancel re-centering var up = VcamState.ReferenceUp; var fwd = GetReferenceFrame(up) * Vector3.forward; PanAxis.Value = 0; var targetFwd = targetRot * Vector3.forward; var a = fwd.ProjectOntoPlane(up); var b = targetFwd.ProjectOntoPlane(up); if (!a.AlmostZero() && !b.AlmostZero()) PanAxis.Value = Vector3.SignedAngle(a, b, up); TiltAxis.Value = 0; fwd = Quaternion.AngleAxis(PanAxis.Value, up) * fwd; var right = Vector3.Cross(up, fwd); if (!right.AlmostZero()) TiltAxis.Value = Vector3.SignedAngle(fwd, targetFwd, right); } Quaternion GetReferenceFrame(Vector3 up) { Transform target = null; switch (ReferenceFrame) { case ReferenceFrames.World: break; case ReferenceFrames.TrackingTarget: target = FollowTarget; break; case ReferenceFrames.LookAtTarget: target = LookAtTarget; break; case ReferenceFrames.ParentObject: target = VirtualCamera.transform.parent; break; } return (target != null) ? target.rotation : Quaternion.FromToRotation(Vector3.up, up); } } }