using UnityEngine; using Cinemachine.Utility; using UnityEngine.Serialization; using System; namespace Cinemachine { /// /// A Cinemachine Camera geared towards a 3rd person camera experience. /// The camera orbits around its subject with three separate camera rigs defining /// rings around the target. Each rig has its own radius, height offset, composer, /// and lens settings. /// Depending on the camera's position along the spline connecting these three rigs, /// these settings are interpolated to give the final camera position and state. /// [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] [DisallowMultipleComponent] [ExecuteAlways] [ExcludeFromPreset] [AddComponentMenu("Cinemachine/CinemachineFreeLook")] [HelpURL(Documentation.BaseURL + "manual/CinemachineFreeLook.html")] public class CinemachineFreeLook : CinemachineVirtualCameraBase { /// Object for the camera children to look at (the aim target) [Tooltip("Object for the camera children to look at (the aim target).")] [NoSaveDuringPlay] [VcamTargetProperty] public Transform m_LookAt = null; /// Object for the camera children wants to move with (the body target) [Tooltip("Object for the camera children wants to move with (the body target).")] [NoSaveDuringPlay] [VcamTargetProperty] public Transform m_Follow = null; /// If enabled, this lens setting will apply to all three child rigs, /// otherwise the child rig lens settings will be used [Tooltip("If enabled, this lens setting will apply to all three child rigs, " + "otherwise the child rig lens settings will be used")] [FormerlySerializedAs("m_UseCommonLensSetting")] public bool m_CommonLens = true; /// Specifies the lens properties of this Virtual Camera. /// This generally mirrors the Unity Camera's lens settings, and will be used to drive /// the Unity camera when the vcam is active [FormerlySerializedAs("m_LensAttributes")] [Tooltip("Specifies the lens properties of this Virtual Camera. This generally " + "mirrors the Unity Camera's lens settings, and will be used to drive the " + "Unity camera when the vcam is active")] public LensSettings m_Lens = LensSettings.Default; /// Collection of parameters that influence how this virtual camera transitions from /// other virtual cameras public TransitionParams m_Transitions; /// Legacy support [SerializeField] [HideInInspector] [FormerlySerializedAs("m_BlendHint")] [FormerlySerializedAs("m_PositionBlending")] private BlendHint m_LegacyBlendHint; /// The Vertical axis. Value is 0..1. Chooses how to blend the child rigs [Header("Axis Control")] [Tooltip("The Vertical axis. Value is 0..1. Chooses how to blend the child rigs")] [AxisStateProperty] public AxisState m_YAxis = new AxisState(0, 1, false, true, 2f, 0.2f, 0.1f, "Mouse Y", false); /// Controls how automatic recentering of the Y axis is accomplished [Tooltip("Controls how automatic recentering of the Y axis is accomplished")] public AxisState.Recentering m_YAxisRecentering = new AxisState.Recentering(false, 1, 2); /// The Horizontal axis. Value is -180...180. This is passed on to /// the rigs' OrbitalTransposer component [Tooltip("The Horizontal axis. Value is -180...180. " + "This is passed on to the rigs' OrbitalTransposer component")] [AxisStateProperty] public AxisState m_XAxis = new AxisState(-180, 180, true, false, 300f, 0.1f, 0.1f, "Mouse X", true); /// The definition of Forward. Camera will follow behind [OrbitalTransposerHeadingProperty] [Tooltip("The definition of Forward. Camera will follow behind.")] public CinemachineOrbitalTransposer.Heading m_Heading = new CinemachineOrbitalTransposer.Heading( CinemachineOrbitalTransposer.Heading.HeadingDefinition.TargetForward, 4, 0); /// Controls how automatic recentering of the X axis is accomplished [Tooltip("Controls how automatic recentering of the X axis is accomplished")] public AxisState.Recentering m_RecenterToTargetHeading = new AxisState.Recentering(false, 1, 2); /// The coordinate space to use when interpreting the offset from the target [Header("Orbits")] [Tooltip("The coordinate space to use when interpreting the offset from the target. " + "This is also used to set the camera's Up vector, which will be maintained " + "when aiming the camera.")] public CinemachineOrbitalTransposer.BindingMode m_BindingMode = CinemachineOrbitalTransposer.BindingMode.SimpleFollowWithWorldUp; /// [Tooltip("Controls how taut is the line that connects the rigs' orbits, which " + "determines final placement on the Y axis")] [Range(0f, 1f)] [FormerlySerializedAs("m_SplineTension")] public float m_SplineCurvature = 0.2f; /// Defines the height and radius of the Rig orbit [Serializable] public struct Orbit { /// Height relative to target public float m_Height; /// Radius of orbit public float m_Radius; /// Constructor with specific values /// Orbit height /// Orbit radius public Orbit(float h, float r) { m_Height = h; m_Radius = r; } } /// The radius and height of the three orbiting rigs [Tooltip("The radius and height of the three orbiting rigs.")] public Orbit[] m_Orbits = new Orbit[3] { // These are the default orbits new Orbit(4.5f, 1.75f), new Orbit(2.5f, 3f), new Orbit(0.4f, 1.3f) }; // Legacy support [SerializeField] [HideInInspector] [FormerlySerializedAs("m_HeadingBias")] private float m_LegacyHeadingBias = float.MaxValue; bool mUseLegacyRigDefinitions = false; /// Enforce bounds for fields, when changed in inspector. protected override void OnValidate() { base.OnValidate(); // Upgrade after a legacy deserialize if (m_LegacyHeadingBias != float.MaxValue) { m_Heading.m_Bias= m_LegacyHeadingBias; m_LegacyHeadingBias = float.MaxValue; int heading = (int)m_Heading.m_Definition; if (m_RecenterToTargetHeading.LegacyUpgrade(ref heading, ref m_Heading.m_VelocityFilterStrength)) m_Heading.m_Definition = (CinemachineOrbitalTransposer.Heading.HeadingDefinition)heading; mUseLegacyRigDefinitions = true; } if (m_LegacyBlendHint != BlendHint.None) { m_Transitions.m_BlendHint = m_LegacyBlendHint; m_LegacyBlendHint = BlendHint.None; } m_YAxis.Validate(); m_XAxis.Validate(); m_RecenterToTargetHeading.Validate(); m_YAxisRecentering.Validate(); m_Lens.Validate(); InvalidateRigCache(); #if UNITY_EDITOR for (int i = 0; m_Rigs != null && i < 3 && i < m_Rigs.Length; ++i) if (m_Rigs[i] != null) CinemachineVirtualCamera.SetFlagsForHiddenChild(m_Rigs[i].gameObject); #endif } /// Get a child rig /// Rig index. Can be 0, 1, or 2 /// The rig, or null if index is bad. public CinemachineVirtualCamera GetRig(int i) { UpdateRigCache(); return (i < 0 || i > 2) ? null : m_Rigs[i]; } /// Names of the 3 child rigs public static string[] RigNames { get { return new string[] { "TopRig", "MiddleRig", "BottomRig" }; } } bool mIsDestroyed = false; /// Updates the child rig cache protected override void OnEnable() { mIsDestroyed = false; base.OnEnable(); InvalidateRigCache(); UpdateInputAxisProvider(); } /// /// API for the inspector. Internal use only /// public void UpdateInputAxisProvider() { m_XAxis.SetInputAxisProvider(0, null); m_YAxis.SetInputAxisProvider(1, null); var provider = GetInputAxisProvider(); if (provider != null) { m_XAxis.SetInputAxisProvider(0, provider); m_YAxis.SetInputAxisProvider(1, provider); } } /// Makes sure that the child rigs get destroyed in an undo-firndly manner. /// Invalidates the rig cache. protected override void OnDestroy() { // Make the rigs visible instead of destroying - this is to keep Undo happy if (m_Rigs != null) foreach (var rig in m_Rigs) if (rig != null && rig.gameObject != null) rig.gameObject.hideFlags &= ~(HideFlags.HideInHierarchy | HideFlags.HideInInspector); mIsDestroyed = true; base.OnDestroy(); } /// Invalidates the rig cache void OnTransformChildrenChanged() { InvalidateRigCache(); } void Reset() { #if UNITY_EDITOR if (RuntimeUtility.IsPrefab(gameObject)) { Debug.Log("You cannot reset a prefab instance. " + "First disconnect this instance from the prefab, or enter Prefab Edit mode"); return; } #endif DestroyRigs(); } /// Set this to force the next update to ignore deltaTime and reset itself public override bool PreviousStateIsValid { get { return base.PreviousStateIsValid; } set { if (value == false) for (int i = 0; m_Rigs != null && i < m_Rigs.Length; ++i) if (m_Rigs[i] != null) m_Rigs[i].PreviousStateIsValid = value; base.PreviousStateIsValid = value; } } /// The cacmera state, which will be a blend of the child rig states override public CameraState State { get { return m_State; } } /// Get the current LookAt target. Returns parent's LookAt if parent /// is non-null and no specific LookAt defined for this camera override public Transform LookAt { get { return ResolveLookAt(m_LookAt); } set { m_LookAt = value; } } /// Get the current Follow target. Returns parent's Follow if parent /// is non-null and no specific Follow defined for this camera override public Transform Follow { get { return ResolveFollow(m_Follow); } set { m_Follow = value; } } /// Check whether the vcam a live child of this camera. /// Returns true if the child is currently contributing actively to the camera state. /// The Virtual Camera to check /// If truw, will only return true if this vcam is the dominat live child /// True if the vcam is currently actively influencing the state of this vcam public override bool IsLiveChild(ICinemachineCamera vcam, bool dominantChildOnly = false) { // Do not update the rig cache here or there will be infinite loop at creation time if (m_Rigs == null || m_Rigs.Length != 3) return false; var y = GetYAxisValue(); if (dominantChildOnly) { if (vcam == (ICinemachineCamera)m_Rigs[0]) return y > 0.666f; if (vcam == (ICinemachineCamera)m_Rigs[2]) return y < 0.333; if (vcam == (ICinemachineCamera)m_Rigs[1]) return y >= 0.333f && y <= 0.666f; return false; } if (vcam == (ICinemachineCamera)m_Rigs[1]) return true; if (y < 0.5f) return vcam == (ICinemachineCamera)m_Rigs[2]; return vcam == (ICinemachineCamera)m_Rigs[0]; } /// This is called to notify the vcam that a target got warped, /// so that the vcam can update its internal state to make the camera /// also warp seamlessy. /// The object that was warped /// The amount the target's position changed public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta) { UpdateRigCache(); if (m_Rigs != null) foreach (var vcam in m_Rigs) vcam.OnTargetObjectWarped(target, positionDelta); base.OnTargetObjectWarped(target, positionDelta); } /// /// Force the virtual camera to assume a given position and orientation. /// Procedural placement then takes over /// /// Worldspace pposition to take /// Worldspace orientation to take public override void ForceCameraPosition(Vector3 pos, Quaternion rot) { var up = State.ReferenceUp; m_YAxis.Value = GetYAxisClosestValue(pos, up); PreviousStateIsValid = true; transform.position = pos; transform.rotation = rot; m_State.RawPosition = pos; m_State.RawOrientation = rot; UpdateRigCache(); if (m_BindingMode != CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp) m_XAxis.Value = mOrbitals[1].GetAxisClosestValue(pos, up); PushSettingsToRigs(); for (int i = 0; i < 3; ++i) m_Rigs[i].ForceCameraPosition(pos, rot); InternalUpdateCameraState(up, -1); base.ForceCameraPosition(pos, rot); } /// Internal use only. Called by CinemachineCore at designated update time /// so the vcam can position itself and track its targets. All 3 child rigs are updated, /// and a blend calculated, depending on the value of the Y axis. /// Default world Up, set by the CinemachineBrain /// Delta time for time-based effects (ignore if less than 0) override public void InternalUpdateCameraState(Vector3 worldUp, float deltaTime) { UpdateTargetCache(); UpdateRigCache(); // Update the current state by invoking the component pipeline m_State = CalculateNewState(worldUp, deltaTime); ApplyPositionBlendMethod(ref m_State, m_Transitions.m_BlendHint); // Push the raw position back to the game object's transform, so it // moves along with the camera. Leave the orientation alone, because it // screws up camera dragging when there is a LookAt behaviour. if (Follow != null) { Vector3 delta = State.RawPosition - transform.position; transform.position = State.RawPosition; m_Rigs[0].transform.position -= delta; m_Rigs[1].transform.position -= delta; m_Rigs[2].transform.position -= delta; } InvokePostPipelineStageCallback(this, CinemachineCore.Stage.Finalize, ref m_State, deltaTime); PreviousStateIsValid = true; // Set up for next frame bool activeCam = PreviousStateIsValid && CinemachineCore.Instance.IsLive(this); if (activeCam && deltaTime >= 0) { if (m_YAxis.Update(deltaTime)) m_YAxisRecentering.CancelRecentering(); } PushSettingsToRigs(); if (m_BindingMode == CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp) m_XAxis.Value = 0; } /// If we are transitioning from another FreeLook, grab the axis values from it. /// 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) public override void OnTransitionFromCamera( ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime) { base.OnTransitionFromCamera(fromCam, worldUp, deltaTime); InvokeOnTransitionInExtensions(fromCam, worldUp, deltaTime); bool forceUpdate = false; // m_RecenterToTargetHeading.DoRecentering(ref m_XAxis, -1, 0); // m_YAxis.m_Recentering.DoRecentering(ref m_YAxis, -1, 0.5f); // m_RecenterToTargetHeading.CancelRecentering(); // m_YAxis.m_Recentering.CancelRecentering(); if (fromCam != null && m_Transitions.m_InheritPosition && !CinemachineCore.Instance.IsLiveInBlend(this)) { var cameraPos = fromCam.State.RawPosition; // Special handling for FreeLook: get an undamped outgoing position if (fromCam is CinemachineFreeLook) { var flFrom = (fromCam as CinemachineFreeLook); var orbital = flFrom.mOrbitals != null ? flFrom.mOrbitals[1] : null; if (orbital != null) cameraPos = orbital.GetTargetCameraPosition(worldUp); } ForceCameraPosition(cameraPos, fromCam.State.FinalOrientation); } if (forceUpdate) { for (int i = 0; i < 3; ++i) m_Rigs[i].InternalUpdateCameraState(worldUp, deltaTime); InternalUpdateCameraState(worldUp, deltaTime); } else UpdateCameraState(worldUp, deltaTime); if (m_Transitions.m_OnCameraLive != null) m_Transitions.m_OnCameraLive.Invoke(this, fromCam); } /// /// Returns true, because FreeLook requires input. /// internal override bool RequiresUserInput() { return true; } float GetYAxisClosestValue(Vector3 cameraPos, Vector3 up) { if (Follow != null) { // Rotate the camera pos to the back Quaternion q = Quaternion.FromToRotation(up, Vector3.up); Vector3 dir = q * (cameraPos - Follow.position); Vector3 flatDir = dir; flatDir.y = 0; if (!flatDir.AlmostZero()) { float angle = Vector3.SignedAngle(flatDir, Vector3.back, Vector3.up); dir = Quaternion.AngleAxis(angle, Vector3.up) * dir; } dir.x = 0; // Sample the spline in a few places, find the 2 closest, and lerp int i0 = 0, i1 = 0; float a0 = 0, a1 = 0; const int NumSamples = 13; float step = 1f / (NumSamples-1); for (int i = 0; i < NumSamples; ++i) { float a = Vector3.SignedAngle( dir, GetLocalPositionForCameraFromInput(i * step), Vector3.right); if (i == 0) a0 = a1 = a; else { if (Mathf.Abs(a) < Mathf.Abs(a0)) { a1 = a0; i1 = i0; a0 = a; i0 = i; } else if (Mathf.Abs(a) < Mathf.Abs(a1)) { a1 = a; i1 = i; } } } if (Mathf.Sign(a0) == Mathf.Sign(a1)) return i0 * step; float t = Mathf.Abs(a0) / (Mathf.Abs(a0) + Mathf.Abs(a1)); return Mathf.Lerp(i0 * step, i1 * step, t); } return m_YAxis.Value; // stay conservative } CameraState m_State = CameraState.Default; // Current state this frame /// Serialized in order to support copy/paste [SerializeField][HideInInspector][NoSaveDuringPlay] private CinemachineVirtualCamera[] m_Rigs = new CinemachineVirtualCamera[3]; void InvalidateRigCache() { mOrbitals = null; } CinemachineOrbitalTransposer[] mOrbitals = null; CinemachineBlend mBlendA; CinemachineBlend mBlendB; /// /// Override component pipeline creation. /// This needs to be done by the editor to support Undo. /// The override must do exactly the same thing as the CreatePipeline method in this class. /// public static CreateRigDelegate CreateRigOverride; /// /// Override component pipeline creation. /// This needs to be done by the editor to support Undo. /// The override must do exactly the same thing as the CreatePipeline method in this class. /// public delegate CinemachineVirtualCamera CreateRigDelegate( CinemachineFreeLook vcam, string name, CinemachineVirtualCamera copyFrom); /// /// Override component pipeline destruction. /// This needs to be done by the editor to support Undo. /// public static DestroyRigDelegate DestroyRigOverride; /// /// Override component pipeline destruction. /// This needs to be done by the editor to support Undo. /// public delegate void DestroyRigDelegate(GameObject rig); private void DestroyRigs() { CinemachineVirtualCamera[] oldRigs = new CinemachineVirtualCamera[RigNames.Length]; for (int i = 0; i < RigNames.Length; ++i) { foreach (Transform child in transform) if (child.gameObject.name == RigNames[i]) oldRigs[i] = child.GetComponent(); } for (int i = 0; i < oldRigs.Length; ++i) { if (oldRigs[i] != null) { if (DestroyRigOverride != null) DestroyRigOverride(oldRigs[i].gameObject); else Destroy(oldRigs[i].gameObject); } } m_Rigs = null; mOrbitals = null; } private CinemachineVirtualCamera[] CreateRigs(CinemachineVirtualCamera[] copyFrom) { // Invalidate the cache mOrbitals = null; float[] softCenterDefaultsV = new float[] { 0.5f, 0.55f, 0.6f }; CinemachineVirtualCamera[] newRigs = new CinemachineVirtualCamera[3]; for (int i = 0; i < RigNames.Length; ++i) { CinemachineVirtualCamera src = null; if (copyFrom != null && copyFrom.Length > i) src = copyFrom[i]; if (CreateRigOverride != null) newRigs[i] = CreateRigOverride(this, RigNames[i], src); else { // Create a new rig with default components // Note: copyFrom only supported in Editor, not build GameObject go = new GameObject(RigNames[i]); go.transform.parent = transform; newRigs[i] = go.AddComponent(); go = newRigs[i].GetComponentOwner().gameObject; go.AddComponent(); go.AddComponent(); } // Set up the defaults newRigs[i].InvalidateComponentPipeline(); CinemachineOrbitalTransposer orbital = newRigs[i].GetCinemachineComponent(); if (orbital == null) orbital = newRigs[i].AddCinemachineComponent(); // should not happen if (src == null) { // Only set defaults if not copying orbital.m_YawDamping = 0; CinemachineComposer composer = newRigs[i].GetCinemachineComponent(); if (composer != null) { composer.m_HorizontalDamping = composer.m_VerticalDamping = 0; composer.m_ScreenX = 0.5f; composer.m_ScreenY = softCenterDefaultsV[i]; composer.m_DeadZoneWidth = composer.m_DeadZoneHeight = 0f; composer.m_SoftZoneWidth = composer.m_SoftZoneHeight = 0.8f; composer.m_BiasX = composer.m_BiasY = 0; } } } return newRigs; } private void UpdateRigCache() { if (mIsDestroyed) return; bool isPrefab = RuntimeUtility.IsPrefab(gameObject); #if UNITY_EDITOR // Special condition: Did we just get copy/pasted? if (m_Rigs != null && m_Rigs.Length == 3 && m_Rigs[0] != null && m_Rigs[0].transform.parent != transform) { if (!isPrefab) // can't paste to a prefab { var copyFrom = m_Rigs; DestroyRigs(); m_Rigs = CreateRigs(copyFrom); } } #endif // Early out if we're up to date if (mOrbitals != null && mOrbitals.Length == 3) return; // Locate existing rigs, and recreate them if any are missing if (LocateExistingRigs(RigNames, false) != 3 && !isPrefab) { DestroyRigs(); m_Rigs = CreateRigs(null); LocateExistingRigs(RigNames, true); } #if UNITY_EDITOR foreach (var rig in m_Rigs) { // Configure the UI if (rig == null) continue; rig.m_ExcludedPropertiesInInspector = m_CommonLens ? new string[] { "m_Script", "Header", "Extensions", "m_Priority", "m_Transitions", "m_Follow", "m_StandbyUpdate", "m_Lens" } : new string[] { "m_Script", "Header", "Extensions", "m_Priority", "m_Transitions", "m_Follow", "m_StandbyUpdate" }; rig.m_LockStageInInspector = new CinemachineCore.Stage[] { CinemachineCore.Stage.Body }; } #endif // Create the blend objects mBlendA = new CinemachineBlend(m_Rigs[1], m_Rigs[0], AnimationCurve.Linear(0, 0, 1, 1), 1, 0); mBlendB = new CinemachineBlend(m_Rigs[2], m_Rigs[1], AnimationCurve.Linear(0, 0, 1, 1), 1, 0); } private int LocateExistingRigs(string[] rigNames, bool forceOrbital) { m_CachedXAxisHeading = m_XAxis.Value; m_LastHeadingUpdateFrame = -1; mOrbitals = new CinemachineOrbitalTransposer[rigNames.Length]; m_Rigs = new CinemachineVirtualCamera[rigNames.Length]; int rigsFound = 0; foreach (Transform child in transform) { CinemachineVirtualCamera vcam = child.GetComponent(); if (vcam != null) { GameObject go = child.gameObject; for (int i = 0; i < rigNames.Length; ++i) { if (mOrbitals[i] == null && go.name == rigNames[i]) { // Must have an orbital transposer or it's no good mOrbitals[i] = vcam.GetCinemachineComponent(); if (mOrbitals[i] == null && forceOrbital) mOrbitals[i] = vcam.AddCinemachineComponent(); if (mOrbitals[i] != null) { mOrbitals[i].m_HeadingIsSlave = true; mOrbitals[i].m_XAxis.m_InputAxisName = string.Empty; mOrbitals[i].HeadingUpdater = UpdateXAxisHeading; mOrbitals[i].m_RecenterToTargetHeading.m_enabled = false; m_Rigs[i] = vcam; m_Rigs[i].m_StandbyUpdate = m_StandbyUpdate; ++rigsFound; } } } } } return rigsFound; } float m_CachedXAxisHeading; float m_LastHeadingUpdateFrame; float UpdateXAxisHeading(CinemachineOrbitalTransposer orbital, float deltaTime, Vector3 up) { if (this == null) return 0; // deleted // Update the axis only once per frame if (m_LastHeadingUpdateFrame != Time.frameCount) { m_LastHeadingUpdateFrame = Time.frameCount; var oldValue = m_XAxis.Value; m_CachedXAxisHeading = orbital.UpdateHeading( PreviousStateIsValid ? deltaTime : -1, up, ref m_XAxis, ref m_RecenterToTargetHeading, CinemachineCore.Instance.IsLive(this)); // Allow externally-driven values to work in this mode if (m_BindingMode == CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp) m_XAxis.Value = oldValue; } return m_CachedXAxisHeading; } void PushSettingsToRigs() { UpdateRigCache(); for (int i = 0; i < m_Rigs.Length; ++i) { if (m_Rigs[i] == null) continue; if (m_CommonLens) m_Rigs[i].m_Lens = m_Lens; // If we just deserialized from a legacy version, // pull the orbits and targets from the rigs if (mUseLegacyRigDefinitions) { mUseLegacyRigDefinitions = false; m_Orbits[i].m_Height = mOrbitals[i].m_FollowOffset.y; m_Orbits[i].m_Radius = -mOrbitals[i].m_FollowOffset.z; if (m_Rigs[i].Follow != null) Follow = m_Rigs[i].Follow; } m_Rigs[i].Follow = null; m_Rigs[i].m_StandbyUpdate = m_StandbyUpdate; m_Rigs[i].FollowTargetAttachment = FollowTargetAttachment; m_Rigs[i].LookAtTargetAttachment = LookAtTargetAttachment; if (!PreviousStateIsValid) { m_Rigs[i].PreviousStateIsValid = false; m_Rigs[i].transform.position = transform.position; m_Rigs[i].transform.rotation = transform.rotation; } #if UNITY_EDITOR // Hide the rigs from prying eyes CinemachineVirtualCamera.SetFlagsForHiddenChild(m_Rigs[i].gameObject); #endif mOrbitals[i].m_FollowOffset = GetLocalPositionForCameraFromInput(GetYAxisValue()); mOrbitals[i].m_BindingMode = m_BindingMode; mOrbitals[i].m_Heading = m_Heading; mOrbitals[i].m_XAxis.Value = m_XAxis.Value; // Hack to get SimpleFollow with heterogeneous dampings to work if (m_BindingMode == CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp) m_Rigs[i].SetStateRawPosition(State.RawPosition); } } private float GetYAxisValue() { float range = m_YAxis.m_MaxValue - m_YAxis.m_MinValue; return (range > UnityVectorExtensions.Epsilon) ? m_YAxis.Value / range : 0.5f; } private CameraState CalculateNewState(Vector3 worldUp, float deltaTime) { CameraState state = PullStateFromVirtualCamera(worldUp, ref m_Lens); m_YAxisRecentering.DoRecentering(ref m_YAxis, deltaTime, 0.5f); // Blend from the appropriate rigs float t = GetYAxisValue(); if (t > 0.5f) { if (mBlendA != null) { mBlendA.TimeInBlend = (t - 0.5f) * 2f; mBlendA.UpdateCameraState(worldUp, deltaTime); state = mBlendA.State; } } else { if (mBlendB != null) { mBlendB.TimeInBlend = t * 2f; mBlendB.UpdateCameraState(worldUp, deltaTime); state = mBlendB.State; } } return state; } /// /// Returns the local position of the camera along the spline used to connect the /// three camera rigs. Does not take into account the current heading of the /// camera (or its target) /// /// The t-value for the camera on its spline. Internally clamped to /// the value [0,1] /// The local offset (back + up) of the camera WRT its target based on the /// supplied t-value public Vector3 GetLocalPositionForCameraFromInput(float t) { if (mOrbitals == null) return Vector3.zero; UpdateCachedSpline(); int n = 1; if (t > 0.5f) { t -= 0.5f; n = 2; } return SplineHelpers.Bezier3( t * 2f, m_CachedKnots[n], m_CachedCtrl1[n], m_CachedCtrl2[n], m_CachedKnots[n+1]); } Orbit[] m_CachedOrbits; float m_CachedTension; Vector4[] m_CachedKnots; Vector4[] m_CachedCtrl1; Vector4[] m_CachedCtrl2; void UpdateCachedSpline() { bool cacheIsValid = (m_CachedOrbits != null && m_CachedOrbits.Length == 3 && m_CachedTension == m_SplineCurvature); for (int i = 0; i < 3 && cacheIsValid; ++i) cacheIsValid = (m_CachedOrbits[i].m_Height == m_Orbits[i].m_Height && m_CachedOrbits[i].m_Radius == m_Orbits[i].m_Radius); if (!cacheIsValid) { float t = m_SplineCurvature; m_CachedKnots = new Vector4[5]; m_CachedCtrl1 = new Vector4[5]; m_CachedCtrl2 = new Vector4[5]; m_CachedKnots[1] = new Vector4(0, m_Orbits[2].m_Height, -m_Orbits[2].m_Radius, 0); m_CachedKnots[2] = new Vector4(0, m_Orbits[1].m_Height, -m_Orbits[1].m_Radius, 0); m_CachedKnots[3] = new Vector4(0, m_Orbits[0].m_Height, -m_Orbits[0].m_Radius, 0); m_CachedKnots[0] = Vector4.Lerp(m_CachedKnots[1], Vector4.zero, t); m_CachedKnots[4] = Vector4.Lerp(m_CachedKnots[3], Vector4.zero, t); SplineHelpers.ComputeSmoothControlPoints( ref m_CachedKnots, ref m_CachedCtrl1, ref m_CachedCtrl2); m_CachedOrbits = new Orbit[3]; for (int i = 0; i < 3; ++i) m_CachedOrbits[i] = m_Orbits[i]; m_CachedTension = m_SplineCurvature; } } // This prevents the sensor size from dirtying the scene in the event of aspect ratio change internal override void OnBeforeSerialize() { if (!m_Lens.IsPhysicalCamera) m_Lens.SensorSize = Vector2.one; } } }