Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question [SOLVED] Dynamically changing CinemachineTargetGroup target weight

Discussion in 'Cinemachine' started by downstroypvp, Jul 19, 2023.

  1. downstroypvp

    downstroypvp

    Joined:
    May 30, 2016
    Posts:
    35
    Hello,

    I have a cinemachine camera setup like this:

    upload_2023-7-19_15-44-31.png

    Follow and LookAt transform are setup at runtime. "Follow" targets the player (targetTransform in the code below) and "LookAt" (_lookAt in the code below) targets a CinemachineTargetGroup, composed of two transform: the player's targetTransform and a _lookAt target positioned a bit ahead of the player

    Code (CSharp):
    1.         /// <summary>
    2.         /// Initialize the follow and look at target
    3.         /// targetTransform is the transform of the player. LookAt is positioned a bit ahead of the player.
    4.         /// </summary>
    5.         protected override void SetupLookAt(Transform targetTransform, Transform lookAt)
    6.         {
    7.             _lookAt = lookAt;
    8.             _targetGroup.AddMember(targetTransform, 0, 0);
    9.             _targetGroup.AddMember(_lookAt, 1, 0);
    The requirement is that by default, the Camera should aim at the LookAt (thus the targetTransform weight set to 0).

    But, in the case the lookAt and the targetTransform can't be both displayed in the camera frustum, the player's targetTransform should take precedence, even if we lose the lookAt for a moment.

    We don't want to use the integrated FOV/ dolly to keep those two targets inside the frustum, we want to increase the weight of the player's targetTransform inside the CinemachineTargetGroup when we can't keep both object in the camera frustum.

    I can do that with the code below:

    Code (CSharp):
    1.  
    2.         #region Inner Struct
    3.         private struct TargetGroupState
    4.         {
    5.             public UnityEngine.Plane[] Planes;
    6.             public Vector3[] Corners;
    7.             public Bounds ColliderBounds;
    8.         }
    9.         #endregion Inner Struct
    10.  
    11.         #region Fields
    12.         #region SerializeField
    13.         /// <summary>
    14.         /// Group Composer.
    15.         /// </summary>
    16.         [SerializeField] private CinemachineTargetGroup _targetGroup;
    17.  
    18.         /// <summary>
    19.         /// The VehicleCinemachineCamera this GroupVehicleHotspot is a hotspot of.
    20.         /// </summary>
    21.         [SerializeField] private VehicleCinemachineCamera _vehicleCinemachineCamera;
    22.         #endregion SerializeField
    23.  
    24.         private Transform _lookAt = null;
    25.         private ValueRecorder _valueRecorder = null;
    26.         private int _followTargetIndex = -1;
    27.         private int _lookAtTargetIndex = -1;
    28.         private float _followWeight = 0.0f;
    29.  
    30.         private TargetGroupState _defaultTargetGroupState;
    31.         private TargetGroupState _currentTargetGroupState;
    32.         #endregion Fields
    33.  
    34.  
    35.        private void Update()
    36.         {
    37.             if (_vehicleCinemachineCamera.Camera == null)
    38.             {
    39.                 return;
    40.             }
    41.  
    42.             // first step is to compute the camera frustum planes when in "default" mode, to see if the player is inside the cameraFrustum when its weight is 0
    43.             _targetGroup.m_Targets[_followTargetIndex].weight = 0.0f;
    44.             _targetGroup.DoUpdate();
    45.  
    46.             // QUESTION: this is where the problem lies. I need to compute the CameraFrustum planes with the followTarget weight set at 0.
    47.             _defaultTargetGroupState.Planes = GeometryUtility.CalculateFrustumPlanes(_vehicleCinemachineCamera.Camera);
    48.             UpdateColliderBounds(ref _defaultTargetGroupState);
    49.  
    50.             if (PointsAreInsideFrustum(_defaultTargetGroupState.Corners, _defaultTargetGroupState.Planes))
    51.             {
    52.                 _followWeight = 0.0f;
    53.                 return;
    54.             }
    55.  
    56.             // This is the second step, in case the player isn't inside the frustum with its weight sets to 0, we update its weight in the CinemachineTargetGroup to keep him in the frustum.
    57.             _targetGroup.m_Targets[_followTargetIndex].weight = _followWeight;
    58.             _targetGroup.DoUpdate();
    59.  
    60.             _currentTargetGroupState.Planes = GeometryUtility.CalculateFrustumPlanes(_vehicleCinemachineCamera.Camera);
    61.             UpdateColliderBounds(ref _currentTargetGroupState);
    62.  
    63.             // if the player is inside the frustum, given its modified weight, decrease its weight, otherwise increase it.
    64.             if (PointsAreInsideFrustum(_currentTargetGroupState.Corners, _currentTargetGroupState.Planes))
    65.             {
    66.                 _followWeight = Mathf.Max(0.0f, _followWeight - 0.1f);
    67.             }
    68.             else
    69.             {
    70.                 _followWeight += 0.1f;
    71.             }
    72.         }
    As noted in the code, I don't know how to compute the camera frustum planes after having reset the weight of the player targetTransform to 0.

    Is there a function somwhere that I can call to get the frustum planes of a camera given the setup I have? Or can I compute them myself?

    Thanks!
     
  2. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,233
    Sorry for the slow response time.

    There is no function you can call. Furthermore, the best that you will be able to do is to compute the camera frustum for the camera as it is now (i.e. in the previous frame). You can do that by using vcam.State.FinalPosition and vcam.State.FinalOrientation and vcam.State.Lens (or, if your setup is relatively simple, you can just use the main camera's transform and lens).

    You can compute whether the a point is off-screen by looking at the horizontal and vertical angels of a line from the camera position to the target, relative to camera forward. Here is some code from CinemachineCollider.cs that you can use as a starting point.

    Code (CSharp):
    1.         static bool IsTargetOffscreen(CameraState state)
    2.         {
    3.             if (state.HasLookAt)
    4.             {
    5.                 Vector3 dir = state.ReferenceLookAt - state.CorrectedPosition;
    6.                 dir = Quaternion.Inverse(state.CorrectedOrientation) * dir;
    7.                 if (state.Lens.Orthographic)
    8.                 {
    9.                     if (Mathf.Abs(dir.y) > state.Lens.OrthographicSize)
    10.                         return true;
    11.                     if (Mathf.Abs(dir.x) > state.Lens.OrthographicSize * state.Lens.Aspect)
    12.                         return true;
    13.                 }
    14.                 else
    15.                 {
    16.                     float fov = state.Lens.FieldOfView / 2;
    17.                     float angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.right), Vector3.forward);
    18.                     if (angle > fov)
    19.                         return true;
    20.  
    21.                     fov = Mathf.Rad2Deg * Mathf.Atan(Mathf.Tan(fov * Mathf.Deg2Rad) * state.Lens.Aspect);
    22.                     angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.up), Vector3.forward);
    23.                     if (angle > fov)
    24.                         return true;
    25.                 }
    26.             }
    27.             return false;
    28.         }
     
  3. downstroypvp

    downstroypvp

    Joined:
    May 30, 2016
    Posts:
    35
    Thanks for the explanation. I was able to achieve what I want using a combination of the IsTargetOnScreen method and a bit of fiddling.

    The extension works like that:
    1- Initialize: setup target, follow and targetgroup. The targetgroup must contains the follow and the target.
    2- UpdateColliderBounds: compute the collider bounds of the target you want to keep onscreen.
    3- UpdateFollowWeight: compute the followWeight increment to be applied to the follow's weight, depending on the corners of the bounding box of the follow and the camera frustum planes.
    4- SetFollowWeighToTargetGroup: add the computed followWeight increment to the targetGroup's follow.

    Here is the resulting extension:

    Code (CSharp):
    1. namespace XXX.Camera.Cinemachine
    2. {
    3.     using global::Cinemachine;
    4.     using global::Cinemachine.Utility;
    5.     using Unity.Physics;
    6.     using UnityEngine;
    7.  
    8.     public class CameraKeepTargetOnScreenGroupComposerWeightExtension : IVehicleCameraCinemachineExtensions<CameraKeepTargetOnScreenGroupComposerWeightSettings>
    9.     {
    10.         #region Inner Struct
    11.         private struct TargetGroupState
    12.         {
    13.             public Vector3[] Corners;
    14.             public Bounds ColliderBounds;
    15.         }
    16.         #endregion Inner Struct
    17.  
    18.         #region SerializeField
    19.         /// <summary>
    20.         /// Group Composer.
    21.         /// </summary>
    22.         [SerializeField] private CinemachineTargetGroup _targetGroup;
    23.  
    24.         /// <summary>
    25.         /// The VehicleCinemachineCamera this GroupVehicleHotspot is a hotspot of.
    26.         /// NOTE: this is just to  way for us to get the aabb bounding box of the target.
    27.         /// </summary>
    28.         [SerializeField] private VehicleCinemachineCamera _vehicleCinemachineCamera;
    29.         #endregion SerializeField
    30.  
    31.         #region Fields
    32.         private const float MAX_WEIGHT = 10.0f;
    33.  
    34.         private int _followTargetIndex = -1;
    35.         private float _followWeight = 0.0f;
    36.  
    37.         private TargetGroupState _targetGroupState;
    38.         #endregion Fields
    39.  
    40.         #region Methods
    41.         public override void Initialize()
    42.         {
    43.             _targetGroupState = new TargetGroupState();
    44.             _targetGroupState.Corners = new Vector3[8];
    45.             _targetGroupState.ColliderBounds = new Bounds();
    46.  
    47.             _followWeight = 0.0f;
    48.  
    49.             _targetGroup.AddMember(VirtualCamera.Follow, _followWeight, 0);
    50.             _targetGroup.AddMember(VirtualCamera.LookAt, 1, 0);
    51.  
    52.             _followTargetIndex = _targetGroup.FindMember(VirtualCamera.Follow);
    53.  
    54.             GetComponent<CinemachineVirtualCameraBase>().LookAt = _targetGroup.transform;
    55.         }
    56.  
    57.         protected override void UpdateCameraState(CinemachineVirtualCameraBase vcam, ref CameraState state, float deltaTime)
    58.         {
    59.             UpdateColliderBounds(ref _targetGroupState);
    60.             UpdateFollowWeight(state, _targetGroupState, ref _followWeight);
    61.             SetFollowWeighToTargetGroup(_followWeight, ref _targetGroup);
    62.         }
    63.  
    64.         /// <summary>
    65.         /// Updates the collider bound and corner for the given targetGroupState.
    66.         /// </summary>
    67.         /// <param name="targetGroupState">The targetGroupState to update the bounds and corners for.</param>
    68.         private void UpdateColliderBounds(ref TargetGroupState targetGroupState)
    69.         {
    70.             Aabb aabb = _vehicleCinemachineCamera.GetColliderLocalBounds();
    71.  
    72.             targetGroupState.ColliderBounds.SetMinMax(VirtualCamera.Follow.TransformPoint(aabb.Min), VirtualCamera.Follow.TransformPoint(aabb.Max));
    73.             /// NOTE: We have a scriptable object Settings that contains a custom scaling of the bounding box of the target, if needed (bigger means the target will never even partially go offscreen, smaller means we can have the target get out of view partially)
    74.             targetGroupState.ColliderBounds.Expand(Settings.TargetBoundScale);
    75.             targetGroupState.ColliderBounds.GetCorners(targetGroupState.Corners);
    76.  
    77.             //Debug.DrawLine(targetGroupState.ColliderBounds.min, targetGroupState.ColliderBounds.max, Color.black, 2.0f);
    78.         }
    79.  
    80.         /// <summary>
    81.         /// Update the follow weight depending on the visibility of the target in the camera frustum. Arbitrarily cap the weight at 10 because it doesn't
    82.         /// affect the camera target past this point.
    83.         /// NOTE: We have a scriptable object Settings that contains an AnimationCurve with the angle difference between frustum planes and target bounding box in x-axis and returns the matching increment in weight to apply to the follow weight in y-axis.
    84.         /// </summary>
    85.         /// <param name="state"> The camera state.</param>
    86.         /// <param name="followWeight"> The followWeight to update.</param>
    87.         private void UpdateFollowWeight(CameraState state, TargetGroupState targetGroupState, ref float followWeight)
    88.         {
    89.             float maxAngleToCameraCorrectedFrustum = GetMaxAngleDistanceFromCameraFrustum(targetGroupState.Corners, state.CorrectedPosition, state.CorrectedOrientation, state.Lens);
    90.             if (maxAngleToCameraCorrectedFrustum > 0.0f)
    91.             {
    92.                 followWeight = Mathf.Min(MAX_WEIGHT, followWeight + Settings.AngleToWeightRatio.Evaluate(maxAngleToCameraCorrectedFrustum));
    93.                 return;
    94.             }
    95.  
    96.             float maxAngleToCameraRawFrustum = GetMaxAngleDistanceFromCameraFrustum(targetGroupState.Corners, state.RawPosition, state.RawOrientation, state.Lens);
    97.             if (maxAngleToCameraRawFrustum <= 0.0f)
    98.             {
    99.                 // Note that we evaluate the ratio based on the corrected camera frustum and not the raw one to avoid jittering.
    100.                 followWeight = Mathf.Min(MAX_WEIGHT, Mathf.Max(0.0f, followWeight + Settings.AngleToWeightRatio.Evaluate(maxAngleToCameraCorrectedFrustum)));
    101.                 return;
    102.             }
    103.         }
    104.  
    105.         /// <summary>
    106.         /// Updates the weight of the target to the given followWeight
    107.         /// </summary>
    108.         /// <param name="followWeight"> The followWeight to set.</param>
    109.         /// <param name="targetGroup"> The target group to update the wfollowWeight for.</param>
    110.         private void SetFollowWeighToTargetGroup(float followWeight, ref CinemachineTargetGroup targetGroup)
    111.         {
    112.             targetGroup.m_Targets[_followTargetIndex].weight = followWeight;
    113.             targetGroup.DoUpdate();
    114.         }
    115.  
    116.         /// <summary>
    117.         /// Check whether all given points are inside the volume defined by frustum from the given camera position, orientation and lens.
    118.         /// Returns the maximum angleToCameraFrustum.
    119.         /// <summary>
    120.         /// <param name="points">The points to check.</param>
    121.         /// <param name="cameraPosition"> The camera position.</param>
    122.         /// <param name="cameraOrientation"> The camera rotation.</param>
    123.         /// <param name="lens">The camera lens.</param>
    124.         /// <returns>A negative number if all points are inside the frustum, a positive number otherwise.</returns>
    125.         private float GetMaxAngleDistanceFromCameraFrustum(Vector3[] points, Vector3 cameraPosition, Quaternion cameraOrientation, LensSettings lens)
    126.         {
    127.             float maxAngleToCameraFrustum = float.MinValue;
    128.  
    129.             foreach (Vector3 point in points)
    130.             {
    131.                 maxAngleToCameraFrustum = Mathf.Max(maxAngleToCameraFrustum, AngleDistanceFromCameraFrustum(point, cameraPosition, cameraOrientation, lens));
    132.             }
    133.  
    134.             return maxAngleToCameraFrustum;
    135.         }
    136.  
    137.         /// <summary>
    138.         /// Returns the maximum angle between the target and the camera frustum angle. If the distance is negative, the target is inside, otherwise it is outside
    139.         /// </summary>
    140.         /// <param name="target"> the point to check if it is onScreen.</param>
    141.         /// <param name="cameraPosition"> The position of the camera from which to test the target point.</param>
    142.         /// <param name="cameraOrientation">The rotation of the camera from which to test the target point.</param>
    143.         /// <param name="lens">The lens of the camera from which to test the target point.</param>
    144.         /// <returns>The angle difference between the target and the camera frustum angle.</returns>
    145.         private float AngleDistanceFromCameraFrustum(Vector3 target, Vector3 cameraPosition, Quaternion cameraOrientation, LensSettings lens)
    146.         {
    147.             Vector3 dir = target - cameraPosition;
    148.             dir = Quaternion.Inverse(cameraOrientation) * dir;
    149.  
    150.             float angleDiffRight;
    151.             float angleDiffUp;
    152.  
    153.             if (lens.Orthographic)
    154.             {
    155.                 angleDiffRight = Mathf.Abs(dir.y) - lens.OrthographicSize;
    156.                 angleDiffUp = Mathf.Abs(dir.x) - lens.OrthographicSize * lens.Aspect;
    157.             }
    158.             else
    159.             {
    160.                 float fov = lens.FieldOfView / 2;
    161.                 float angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.right), Vector3.forward);
    162.                 angleDiffRight = angle - fov;
    163.  
    164.                 fov = Mathf.Rad2Deg * Mathf.Atan(Mathf.Tan(fov * Mathf.Deg2Rad) * lens.Aspect);
    165.                 angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.up), Vector3.forward);
    166.                 angleDiffUp = angle - fov;
    167.             }
    168.  
    169.             return Mathf.Max(angleDiffRight, angleDiffUp);
    170.         }
    171.         #endregion Methods
    172.     }
    173. }
    174.  

    Note that the VehicleCinemachineCamera is a custom class in our project, but is only used to get the bounding box of the target. If you want to use this extension as-is, you will have to pass this bounding box somehow to this extension.

    I have changed the IsTargetOffscreen method to return the maximum angle between the frustum planes and the given point, so that I can dynamically change the increment in followWeight to be applied to the follow in the TargetGroup.

    Here is an example of the settings I use:

    upload_2023-9-29_9-16-32.png


    You will have to tweak this curve to suit your need. Changing the y-axis values (the maximum and minimum followWeight increment) can let you control if you want your target completely onscreen no matter what, or if you allow it to be partially out of camera frustum.


    Thanks you very much @Gregoryl for the time you took to answer.
     
    Lars-Steenhoff likes this.