Search Unity

Weird rotations when blending between two cameras

Discussion in 'Cinemachine' started by mopcleex, Apr 11, 2022.

  1. mopcleex

    mopcleex

    Joined:
    Jul 13, 2017
    Posts:
    6
    I'm creating a game about traversing the surface of a cube, and when transitioning between faces I'm blending between the current virtual camera, and a rotated clone. For some transitions this works fantastic, but for others, Cinemachine adds this weird rotation around the camera's local forward.



    As seen in the video, the first and third transition work as planned, but the transition to the back face, and the transition to at the end have this issue.
    I've tried, but couldn't understand why this is happening. I've debugged the transforms of the two cameras, and mathematically, the most efficient transition should always incorporate a rotation around one axis only, yet for some reason this anomaly occurs.

    Is there something I've missed that could cause this issue? Or perhaps a way to constrain the blend?
    All help appreciated!
     

    Attached Files:

  2. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,719
    Would you be willing to upload your test project? It will be much easier to take a quick look at that than to try to reverse-engineer what you did.
     
  3. mopcleex

    mopcleex

    Joined:
    Jul 13, 2017
    Posts:
    6
    I haven't really tried to share a project before... hope this works
     

    Attached Files:

  4. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,719
    Thanks. I'll have a look.

    Yikes!! CM 2.1.10? That's from 2017! We don't support that version any more.
    What version of Unity are you using?

    When sending a project, you should zip everything except the Library folder. That way, the context is communicated fully. However, I've managed to open your assets in 2020.3.30f1. I'll take a closer look in the coming days.
     
  5. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,719
    You can make the weird rotations go away by setting the LookAt target in the vcams to be the player. The lookAt target serves as a hint for the camera blending code, telling it to blend in such a way as to keep the target onscreen.

    In your CameraController.cs:
    Code (CSharp):
    1.     void InitializeVirtualCamera(CinemachineVirtualCamera vCam, int priority)
    2.     {
    3.         //Variables
    4.         Vector3 playerUp = player.normal();
    5.         Quaternion faceRotation = Quaternion.FromToRotation(Vector3.up, playerUp);
    6.  
    7.         //initialize transposer
    8.         var transposer = vCam.GetTransposer();
    9.         transposer.m_FollowOffset = faceRotation * followOffset;
    10.  
    11.         //position camera
    12.         vCam.transform.parent = transform;
    13.         vCam.transform.position = player.transform.position + transposer.m_FollowOffset;
    14.         vCam.transform.LookAt(player.transform, playerUp);
    15.  
    16.         //assign player follow
    17.         vCam.Follow = player.transform;
    18.         vCam.LookAt = player.transform; // <---- here
    19.         vCam.Priority = priority;
    20.     }
    That said, it's a little unclear to me what exactly you're trying to do. How is the player intended to move? Are the direction keys supposed to be relative to the camera direction (e.g. forward always moves the player into the screen)?

    Do you always want to keep the camera behind the player (i.e. looking from the player's -Z direction)?

    It would help if you could describe in words how you want the player to move in response to the input keys, and where you want the camera to be relative to the player. I suspect that your camera code can be considerably simplified.
     
    Last edited: Apr 13, 2022
  6. mopcleex

    mopcleex

    Joined:
    Jul 13, 2017
    Posts:
    6
    Lol, I started using my PC again after a long time and I'm using whatever versions are set as default, so I'm using Unity version 2020.3.17f1.
    And also, do think upgrading Cinemachine might help?

    Anyways, I tried to add the suggested "LookAt" line, and it seems to mess up all the transitions. Either by not looking at the player at all, or, with both cameras aim settings set to "Do Nothing", the follow works but all transitions still have the problem. I couldn't figure this out, so I deleted the aim constraint.

    As for my intentions mechanics-wise, the player cube rolls freely on the current face of the big cube, and when trying to move to a different face the coroutine "ChangeFaces" described below executes, which does the following things:

    Code (CSharp):
    1.     public IEnumerator ChangeFaces(Vector3 targetPosition, Vector3 previousNormal, Vector3 targetNormal)
    2.     {
    3.         SmartInput.InputManager.freezeInput = true;
    4.  
    5.         MoveInactiveCamera(targetPosition, previousNormal, targetNormal);
    6.         SwitchCameras();
    7.  
    8.         yield return new WaitForSeconds(cmBrain.m_DefaultBlend.m_Time);
    9.  
    10.         MatchCameras();
    11.  
    12.         SmartInput.InputManager.freezeInput = false;
    13.     }
    MoveInactiveCamera - The currently inactive camera is moved to its desired new position which is calculated by rotating the follow offset of the active camera a rotation I get from the normals of the new and current face. Then the camera is rotated using the Transform.LookAt function.

    Code (CSharp):
    1.     public void MoveInactiveCamera(Vector3 targetPosition, Vector3 previousNormal, Vector3 targetNormal)
    2.     {
    3.         Debug.Log($"previous: {previousNormal}, target: {targetNormal}");
    4.  
    5.         Quaternion faceRotation = Quaternion.FromToRotation(previousNormal, targetNormal);
    6.  
    7.         var transposer = inactiveCamera().GetTransposer();
    8.         transposer.m_FollowOffset = faceRotation * transposer.m_FollowOffset;
    9.  
    10.         inactiveCamera().transform.position = targetPosition + transposer.m_FollowOffset;
    11.         inactiveCamera().transform.LookAt(targetPosition, targetNormal);
    12.     }
    SwitchCameras - The cameras switch priorities and the blend begins

    Code (CSharp):
    1.     public void SwitchCameras()
    2.     {
    3.         virtualCamera1.Priority = 1 - virtualCamera1.Priority;
    4.         virtualCamera2.Priority = 1 - virtualCamera2.Priority;
    5.     }
    MatchCameras - The now inactive camera's transform is set to match that of the active one

    Code (CSharp):
    1.     public void MatchCameras()
    2.     {
    3.         //Match Values : follow offset, position & rotation
    4.         inactiveCamera().GetTransposer().m_FollowOffset = activeCamera().GetTransposer().m_FollowOffset;
    5.         inactiveCamera().transform.position = activeCamera().transform.position;
    6.         inactiveCamera().transform.rotation = activeCamera().transform.rotation;
    7.     }
    Afterwards, the input manager recalculates the input bindings based on the camera's local forward and right vectors projected on the current face's plane.


    Code (CSharp):
    1. public void RecalculateInputs(Vector3 normal)
    2.         {
    3.             Vector3 _right = Vector3.ProjectOnPlane(cameraPivotTransform.right, normal);
    4.             right.direction = _right.normalized.toV3Int();
    5.  
    6.             Vector3 _left = Vector3.ProjectOnPlane(-cameraPivotTransform.right, normal);
    7.             left.direction = _left.normalized.toV3Int();
    8.  
    9.             Vector3 _up = Vector3.ProjectOnPlane(cameraPivotTransform.forward, normal);
    10.             up.direction = _up.normalized.toV3Int();
    11.  
    12.             Vector3 _down = Vector3.ProjectOnPlane(-cameraPivotTransform.forward, normal);
    13.             down.direction = _down.normalized.toV3Int();
    14.             }
    15.         }
    The entire process executes from a function in the player script:

    Code (CSharp):
    1.     protected override void MoveInDirection(Vector3Int direction)
    2.     {
    3.         Vector3 prevNormal = normal();
    4.         cubidCoordinate = cubidCoordinate + direction;
    5.  
    6.         bool faceChange = normal() != prevNormal;
    7.  
    8.         //move to new world position
    9.         RotateCube(GetPosition(), faceChange);
    10.  
    11.         OnMove?.Invoke(faceChange, normal());
    12.  
    13.         if (faceChange)
    14.         {
    15.             StartCoroutine(cameraController.ChangeFaces(GetPosition(), prevNormal, normal()));
    16.  
    17.             inputManager.RecalculateInputs(normal());
    18.         }
    19.     }
    Sorry this is all so messy... thanks again for your help!
     
  7. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,719
    Right, but you haven't answered my question. I was asking you to describe in words how you want the camera to position itself relative to the player when the player goes over an edge, and how you want the direction keys to be interpreted. I don't want to know what you did, I want to know what you want.

    When the plyer is moving forwards into the screen and goes over an edge, it's pretty clear that player's local forward should remain the same on the new face, and camera should stay behind the player. ("behind" means positioned somewhere in the player's local -z hemisphere).

    What should happen when the player moves sideways over an edge? And backwards? What should happen to the player's local forward? What should happen to the camera?

    Pictures will do if words are not clear enough. Don't show me code (I already have your project).
     
  8. mopcleex

    mopcleex

    Joined:
    Jul 13, 2017
    Posts:
    6
    Oops... Sorry and thank you for your patience. I hope this will be clearer.

    First, two conventions:
    1. The cube has six faces which will each be called +x, -x, +y, -y, +z and -z based on their normals. (In the image below)
    2. The script I wrote has a vector3 variable which is my desired camera follow offset. I will call it
    offset
    .

    HelpVis2.png

    So the camera should follow the player at
    offset
    - that's standard. However, assigning a constant offset cannot work (or at least doesn't work for me) since when moving to a different face the camera might enter the cube. For example, an offset that looks like (0,0,+n) with n being some number, will force the camera to enter the cube when the player moves to the -x face.

    Optimally, what should happen to avoid this issue is that the camera will update it's follow offset by applying rotations to
    offset
    based on the player's current transform. Now I assume one way to achieve this is by using the "Lock to Target With World Up" binding mode on the virtual camera and assigning the CinemachineBrain's World Up Override to be the player's transform, but I couldn't get that to work for some reason.

    Instead, what I did as seen in my code is having two virtual cameras, and every time the player switches faces I update the inactive camera's follow offset by rotating the current offset (which is derived from the initial value
    offset
    ) by a rotation that is equal to the rotation from the current to the target normal.

    For example, presume the player is on the +y face and wants to move to the +x face - the appropriate rotation will be of 90 degrees around the positive z vector, and that is the exact rotation that should be applied to the camera's follow offset.

    HelpVis.png

    So, this calculation works; the inactive camera moves to the new position and assigned a new offset based on the rotation. The problem is that that is the only rotation that I want to apply to the camera while blending, and that doesn't happen. For instance, in case of the example above (which can be replicated by moving backwards [or towards the camera] when the game starts), the only rotation that should be applied to the camera is the before-mentioned 90 degree around the positive z axis.

    In conclusion, yes, the camera should optimally always stay in the overlapping region between the player's local -z and +y hemispheres, but I feel like there should be a way to achieve this globally, without even considering those constraints.
    If there is something I misunderstood about the math, some insight into Cinemachine blends or information about the binding modes which you could help me with I'll be grateful. Also thanks again for your patience, and if any further clarifications are required I'll be happy to provide them.
     
  9. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,719
    Thanks for the writeup, but I'm still having to guess at some stuff.

    My thought is that you can do everything very elegantly with a single virtual camera properly set up, no blending, camera switches, or math necessary.

    The way you've set up the project, you have a player object that slides along the face when it moves, keeping its Y up and its z pointing away from the camera (the rolling cube is just a decoration on top of that). That's great, because you can use it as an anchor for the camera. When it rolls over an edge, its new orientation should define where the camera will go. It's very simple to make the vcam position itself properly in accordance with that player object's rotation.

    The problem I'm having now with your project is that the player object sometimes ends up facing in what to me is a weird direction when it goes sideways over an edge, and that messes up both the camera and the function of the arrow keys. That's why I'm asking you to clarify what it's supposed to do, because maybe it's intentional. I don't know.

    For me, the movement illustrated below makes sense. Notice the movement of the player object, how its forward is preserved when it rolls over an edge. That's what I would expect. See how the camera follows it nicely. It's just a single camera (the other CM camera icons you see are your blending cameras, but they are not used in my version).

    cube1.gif


    cube2.gif

    Here is what I did:

    1. Create a new empty GameObject called DampedTarget. The purpose of this is to be a intermediary target to smooth the player's rotation when it changes faces, otherwise the cameras would pop along with the player. It has a simple custom script, which is this:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class DampedTarget : MonoBehaviour
    4. {
    5.     public Transform Target;
    6.     public float Damping;
    7.  
    8.     private void Start()
    9.     {
    10.         transform.position = Target.position;
    11.         transform.rotation = Target.rotation;
    12.     }
    13.     void LateUpdate()
    14.     {
    15.         transform.position = Target.position;
    16.         transform.rotation = Quaternion.Slerp(
    17.             transform.rotation, Target.rotation,
    18.             Cinemachine.Utility.Damper.Damp(1, Damping, Time.deltaTime));
    19.     }
    20. }
    21.  
    upload_2022-4-19_20-27-56.png

    2. Make a single vcam, set it up like this (I gave it a higher priority than your other cameras, so that it gets used instead of them). Tune the numbers to get the offset and damping you want:

    upload_2022-4-19_20-28-47.png

    3. Put Player in CMBrain's World Up Override:

    upload_2022-4-19_20-29-30.png

    If you set things up like this, all you have to do is properly control the position and orientation of the Player object (that part of your project needs some work, IMO). The camera is a slave of the player object, and will take care of itself.

    PS: and yes, FOR SURE upgrade to a newer version of Cinemachine using Package Manager (delete the old one from your assets first, or you will regret it).
     
    Last edited: Apr 20, 2022
  10. mopcleex

    mopcleex

    Joined:
    Jul 13, 2017
    Posts:
    6
    Well I tried the on camera setup you suggested, and at first it didn't work because the old version of Cinemachine I was using didn't have the 3d Person binding mode. After an update and a tiny change to my movement script, I managed to get it working how I wanted with these settings:

    HelpVis3.png

    Anyways, It works! Thanks again for everything!
     
    Gregoryl likes this.