Decreasing FoV makes camera look where it's not supposed to (879677)

Hi everyone!

DESCRIPTION

I am using a script I got from Unity’s forums to move my camera. I tried to zoom in by decreasing the FoV and making the transform look at the cursor. The point I am aiming at is correct as tested using CreatePrimitive() on it.

PROBLEM

At first it works and the camera looks at the right spot at first but then something happens in RotateCameraWithMouse() that makes the camera look at the wrong position. Video 8aiil0

The problem seems to be related to the line I marked with // ************** SEEMINGLY THE PROBLEMATIC LINE *************** //. If I comment it out, the zoom doesn’t throw the aim off but the camera can’t move vertically and the horizontal rotation becomes very sensitive to mouse movements.

SOME OF THE THINGS I TRIED

  • Turning mouse smooth off
  • Correcting the clamp amount to compensate for the FoV change, which I hadn’t done before
  • Changing the targetDirection to the post-zoom camera rotation

MINIMUM REPRODUCIBLE EXAMPLE

I know you guys have far better things to do than debug other people’s projects but in case an angel among you is feeling extra bored, I have attached a minimum reproducible example below as a .unitypackage:

https://easyupload.io/skwhi0

Thank you all very much

SCRIPT

using UnityEngine;

public class MouseLook : MonoBehaviour
{
    private Vector2 _mouseAbsolute;
    private Vector2 _smoothMouse;
    public Vector2 initialMaxCameraRotation = new Vector2(40, 30);
    public Vector2 currentMaxCameraRotation;
    public Vector2 sensitivity = new Vector2(0.1f, 0.1f);
    public Vector2 smoothing = new Vector2(3, 3);
    private Quaternion initialCameraRotation;
    private MainCamera mainCamera;
    public bool isZoomActive = false;
    private float zoomFOVDelta; // number of degrees subtracted from normal camera's vertical FoV when zooming in
    private float zoomFOVValue = 10;

    void Start()
    {
        // limit the camera rotation to where the game takes place
        currentMaxCameraRotation = initialMaxCameraRotation;
        // initial direction the camera is facing
        initialCameraRotation = transform.localRotation;
        mainCamera = gameObject.GetComponent<MainCamera>();
        // difference in horizontal field of view after zoom is applied
        zoomFOVDelta = mainCamera.startingHorizontalFieldOfView - Camera.VerticalToHorizontalFieldOfView(10, mainCamera.mainCamera.aspect);
    }

    void Update()
    {
        ZoomIfRightButtonHeldDown();
        RotateCameraWithMouse();
    }

    void ZoomIfRightButtonHeldDown()
    {
        // ##### START ZOOMING #####
        if (Input.GetMouseButton(1))
        {
            // determine where the mouse was so that we can make the camera look at it
            int layerMask = 1 << 8; // ground layer
            Ray mouseClick = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            Physics.Raycast(mouseClick, out hitInfo, Mathf.Infinity, layerMask);

            // no need to apply zoom multiple times
            if (false == isZoomActive)
            {
                // ##### ZOOM AND LOOK AT POINT #####
                mainCamera.mainCamera.fieldOfView = zoomFOVValue;
                mainCamera.transform.LookAt(hitInfo.point);

                // since the FoV changed, the maximum rotation the camera can perform should change as well so it can see the same area
                currentMaxCameraRotation.y += zoomFOVDelta;
                currentMaxCameraRotation.x += mainCamera.startingHorizontalFieldOfView - Camera.VerticalToHorizontalFieldOfView(zoomFOVValue, mainCamera.mainCamera.aspect);

                //DEBUG: check if this is the point you want to look at and add visual reference to compare
                GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                cube.transform.position = hitInfo.point;
            }

            isZoomActive = true;
        }
        // ##### ZOOM OFF #####
        else
        {
            // change the clamp back since FoV has been increased so it can see the same area
            currentMaxCameraRotation = initialMaxCameraRotation;
            // change the FoV back to its initial value
            mainCamera.mainCamera.fieldOfView = mainCamera.startingVerticalFieldOfView;
            isZoomActive = false;
        }
    }

    void RotateCameraWithMouse()
    {
        // Get raw mouse input for a cleaner reading on more sensitive mice.
        var mouseDelta = new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y"));

        // ##### SMOOTH #####
        // Scale input against the sensitivity setting and multiply that against the smoothing value.
        mouseDelta = Vector2.Scale(mouseDelta, new Vector2(sensitivity.x * smoothing.x, sensitivity.y * smoothing.y));
        // Interpolate mouse movement over time to apply smoothing delta.
        _smoothMouse.x = Mathf.Lerp(_smoothMouse.x, mouseDelta.x, 1f / smoothing.x);
        _smoothMouse.y = Mathf.Lerp(_smoothMouse.y, mouseDelta.y, 1f / smoothing.y);

        // Find the absolute mouse movement value from point zero.
        _mouseAbsolute += _smoothMouse;

        // ##### CLAMP CAMERA ROTATION #####
        // Clamp and apply the local x value first, so as not to be affected by world transforms.
        if (currentMaxCameraRotation.x < 360)
            _mouseAbsolute.x = Mathf.Clamp(_mouseAbsolute.x, -currentMaxCameraRotation.x * 0.5f, currentMaxCameraRotation.x * 0.5f);
        // Then clamp and apply the global y value.
        if (currentMaxCameraRotation.y < 360)
            _mouseAbsolute.y = Mathf.Clamp(_mouseAbsolute.y, -currentMaxCameraRotation.y * 0.5f, currentMaxCameraRotation.y * 0.5f);

        // ##### APPLY FINAL CAMERA ROTATION #####
        // ************** SEEMINGLY THE PROBLEMATIC LINE *************** //
        transform.localRotation = Quaternion.AngleAxis(-_mouseAbsolute.y, initialCameraRotation * Vector3.right) * initialCameraRotation;
        var yRotation = Quaternion.AngleAxis(_mouseAbsolute.x, transform.InverseTransformDirection(Vector3.up));
        transform.localRotation *= yRotation;
    }
}

What exactly is “the right spot”? The camera is never looking at the cursor, if that’s what you mean; it just happens to be turning enough that the cursor is still on screen. The spot in the center of the screen appears to be consistent when you decrease the FOV.

Maybe you need to better describe what “the right spot” is supposed to be?

Yeah, the code is correct but the user is confused and assuming the camera is pointing at a freely moving crosshair (it’s not).

The camera isn’t pointing where you clicked because you are explicitly telling it not to. Those ‘problematic lines’ set the rotation of the camera.

Maybe your confusion comes from the fact you refer to what is actually the same transform in two different ways:

transform
and
mainCamera.transform

Since mainCamera is another component on the same object, you’re talking about the same thing. In your Zoom function you tell it to point at the clicked point, and then immediately afterwards you tell it to point somewhere else based on the mouse position.

Try this:

  • Make MainCamera the child of a new empty gameobject. Ensure the new gameobject is at the same position and rotation as the camera was. Easiest way to do this is to right click on MainCamera and select CreateEmpty, then drag the new game object out, then the MainCamera object in as a child. (CreateEmptyParent doesn’t work the same)
  • Put your MouseLook script on the new parent game object instead of MainCamera
  • Use the following edited version of your code:
using UnityEngine;

public class MouseLook : MonoBehaviour
{
    private Vector2 _mouseAbsolute;
    private Vector2 _smoothMouse;
    public Vector2 initialMaxCameraRotation = new Vector2(40, 30);
    public Vector2 currentMaxCameraRotation;
    public Vector2 sensitivity = new Vector2(0.1f, 0.1f);
    public Vector2 smoothing = new Vector2(3, 3);
    private Quaternion initialCameraRotation;
    private MainCamera mainCamera;
    public bool isZoomActive = false;
    private float zoomFOVDelta; // number of degrees subtracted from normal camera's vertical FoV when zooming in
    private float zoomFOVValue = 10;

    void Start()
    {
        Application.targetFrameRate = 60;
        // limit the camera rotation to where the game takes place
        currentMaxCameraRotation = initialMaxCameraRotation;
        // initial direction the camera is facing
        initialCameraRotation = transform.localRotation;
        mainCamera = gameObject.GetComponentInChildren<MainCamera>();
        // difference in horizontal field of view after zoom is applied
        zoomFOVDelta = mainCamera.startingHorizontalFieldOfView - Camera.VerticalToHorizontalFieldOfView(10, mainCamera.mainCamera.aspect);
    }

    void Update()
    {
        ZoomIfRightButtonHeldDown();
        RotateCameraWithMouse();
    }

    void ZoomIfRightButtonHeldDown()
    {
        // ##### START ZOOMING #####
        if (Input.GetMouseButton(1))
        {
            // determine where the mouse was so that we can make the camera look at it
            int layerMask = 1 << 8; // ground layer
            Ray mouseClick = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            Physics.Raycast(mouseClick, out hitInfo, Mathf.Infinity, layerMask);

            // no need to apply zoom multiple times
            if (false == isZoomActive)
            {
                // ##### ZOOM AND LOOK AT POINT #####
                mainCamera.mainCamera.fieldOfView = zoomFOVValue;
                mainCamera.transform.LookAt(hitInfo.point);

                // since the FoV changed, the maximum rotation the camera can perform should change as well so it can see the same area
                //currentMaxCameraRotation.y += zoomFOVDelta;
                //currentMaxCameraRotation.x += mainCamera.startingHorizontalFieldOfView - Camera.VerticalToHorizontalFieldOfView(zoomFOVValue, mainCamera.mainCamera.aspect);

                //DEBUG: check if this is the point you want to look at and add visual reference to compare
                GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                cube.transform.position = hitInfo.point;
            }

            isZoomActive = true;
        }
        // ##### ZOOM OFF #####
        else
        {
            mainCamera.transform.localRotation = Quaternion.identity;
            // change the clamp back since FoV has been increased so it can see the same area
            //currentMaxCameraRotation = initialMaxCameraRotation;
            // change the FoV back to its initial value
            mainCamera.mainCamera.fieldOfView = mainCamera.startingVerticalFieldOfView;
            isZoomActive = false;
        }
    }

    void RotateCameraWithMouse()
    {
        // Get raw mouse input for a cleaner reading on more sensitive mice.
        var mouseDelta = new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y"));

        // ##### SMOOTH #####
        // Scale input against the sensitivity setting and multiply that against the smoothing value.
        mouseDelta = Vector2.Scale(mouseDelta, new Vector2(sensitivity.x * smoothing.x, sensitivity.y * smoothing.y));
        // Interpolate mouse movement over time to apply smoothing delta.
        _smoothMouse.x = Mathf.Lerp(_smoothMouse.x, mouseDelta.x, 1f / smoothing.x);
        _smoothMouse.y = Mathf.Lerp(_smoothMouse.y, mouseDelta.y, 1f / smoothing.y);

        // Find the absolute mouse movement value from point zero.
        _mouseAbsolute += _smoothMouse;

        // ##### CLAMP CAMERA ROTATION #####
        // Clamp and apply the local x value first, so as not to be affected by world transforms.
        if (currentMaxCameraRotation.x < 360)
            _mouseAbsolute.x = Mathf.Clamp(_mouseAbsolute.x, -currentMaxCameraRotation.x * 0.5f, currentMaxCameraRotation.x * 0.5f);
        // Then clamp and apply the global y value.
        if (currentMaxCameraRotation.y < 360)
            _mouseAbsolute.y = Mathf.Clamp(_mouseAbsolute.y, -currentMaxCameraRotation.y * 0.5f, currentMaxCameraRotation.y * 0.5f);

        // ##### APPLY FINAL CAMERA ROTATION #####
        // ************** SEEMINGLY THE PROBLEMATIC LINE *************** //

        transform.localRotation = Quaternion.AngleAxis(-_mouseAbsolute.y, initialCameraRotation * Vector3.right) * initialCameraRotation;
        var yRotation = Quaternion.AngleAxis(_mouseAbsolute.x, transform.InverseTransformDirection(Vector3.up));
        transform.localRotation *= yRotation;
      
    }
}

All I’ve done is made it find the MainCamera script in the child object, commented out the lines limiting rotation differently (you don’t need them), and added a line zeroing the localrotation of the MainCamera when you are not zooming.

This allows you to look around, zoom in with the cube centred, and then continue to look around while zoomed.

Thank you all very much for replying!!

Thank you so much!!! It works perfectly. You, my good sir, are a genius, a gentleman and a scholar!!! Thank you a million times over