Monogame UWP 3D Camera with mouse Quaternion rotation not working

I have been trying for almost a week to implement a working FPS camera in my Monogame UWP project but I have been unable to get anything working properly.

As I am a novice Monogame developer i’m still getting to grips with how Camera logic and Quaternion/Matrix rotations work. I used tutorials to boost me along, but nothing seems to work, so apologies for my code.

One thing that I did notice is that Mouse.SetPosition(centerX, centerY) does not appear to work properly in my Monogame UWP project, which messes up all the examples that i’ve tried so far.

I have created a Mouse workaround via much googling but I am unsure if this workaround is decent. I’m mostly afraid my logic in general or Quaternion rotation code is flawed.

My attempt at updating the LookRotation Quaternion each frame results in the camera snapping back to the centerXY position as I move the mouse.
My other attempt of multiplying LookRotation Quaternion with the current frame ends up in crazy inertia and spinning.
I’ve tried Slerp but it snaps back to the center.

Please see my code below. Not sure how to properly format the code in the post sorry.

class CameraV1
{
// Constants
private const float DEFAULT_MOUSE_SMOOTHING_SENSITIVITY = 0.5f;
private const float DEFAULT_SPEED_ROTATION = 0.2f;//0.2f;
private const int MOUSE_SMOOTHING_CACHE_SIZE = 10;
private const float DEFAULT_PLAYER_HEAD_OFFSET = 1.8f;

    private Viewport Viewport { get; set; }
    private float aspectRatio;

    public Matrix View { get; private set; }
    public Matrix Projection { get; private set; }

    // Set the direction the camera points without rotation.
    Vector3 cameraReference = new Vector3(0, 0, 10);

    Vector3 thirdPersonReference = new Vector3(0, 500, -500);

    // Set field of view of the camera in radians (pi/4 is 45 degrees).
    static float viewAngle = MathHelper.ToRadians(80);// MathHelper.PiOver4;

    // Set distance from the camera of the near and far clipping planes.
    //static float nearClip = 5.0f;
    static float nearClip = 1.0f;
    static float farClip = 2000.0f;

    // Set the camera state, avatar's center, first-person, third-person.
    int cameraState;
    bool cameraStateKeyDown;

    // Set rates in world units per 1/60th second (the default fixed-step interval).
    private static float ForwardSpeed = 50f / 60f;
    private static float StrafeSpeed = 25f / 60f;

    // Set the avatar position and rotation variables.
    public Vector3 PlayerPosition { get; set; }
    private Vector3 PlayerHeadOffset { get; set; }
    private Quaternion LookRotation;
    private Quaternion LookOrientation;

    private float accumYawDegrees;
    private float accumPitchDegrees;

    private bool enableMouseSmoothing;
    private int mouseIndex;
    private float rotationSpeed;
    private float mouseSmoothingSensitivity;
    private Vector2[] mouseMovement;
    private Vector2[] mouseSmoothingCache;
    private Vector2 smoothedMouseMovement;

    private MouseState currentMouseState;
    private MouseState previousMouseState;
    private KeyboardState currentKeyboardState;
    private KeyboardState previousKeyboardState;
    private GamePadState currentGamePadState;

    public CameraV1(Viewport viewport, Vector3 playerPosition)
    {
        Viewport = viewport;
        View = new Matrix();
        Projection = new Matrix();
        PlayerPosition = playerPosition;
        PlayerHeadOffset = new Vector3(0f, DEFAULT_PLAYER_HEAD_OFFSET, 0);

        LookRotation = Quaternion.Identity;
        LookOrientation = Quaternion.Identity;

        aspectRatio = (float)Viewport.Width / (float)Viewport.Height;

        // Initialize camera state.
        accumYawDegrees = 0.0f;
        accumPitchDegrees = 0.0f;

        // Initialize mouse and keyboard input.
        enableMouseSmoothing = false;
        rotationSpeed = DEFAULT_SPEED_ROTATION;
        mouseSmoothingSensitivity = DEFAULT_MOUSE_SMOOTHING_SENSITIVITY;
        mouseSmoothingCache = new Vector2[MOUSE_SMOOTHING_CACHE_SIZE];
        mouseIndex = 0;
        mouseMovement = new Vector2[2];
        mouseMovement[0].X = 0.0f;
        mouseMovement[0].Y = 0.0f;
        mouseMovement[1].X = 0.0f;
        mouseMovement[1].Y = 0.0f;
        smoothedMouseMovement = Vector2.Zero;

        // Get initial keyboard and mouse states.
        currentKeyboardState = Keyboard.GetState();
        currentMouseState = Mouse.GetState();

    }

    public void Update(GameTime gameTime)
    {
        UpdateCameraState();
        UpdateInput();
        UpdatePosition(gameTime);

    }

    private void UpdateCameraState()
    {
        // Toggle the state of the camera.
        if (currentKeyboardState.IsKeyDown(Keys.V) || (currentGamePadState.Buttons.LeftShoulder == ButtonState.Pressed))
        {
            cameraStateKeyDown = true;
        }
        else if (cameraStateKeyDown == true)
        {
            cameraStateKeyDown = false;
            cameraState += 1;
            cameraState %= 3;
        }
    }

    private void UpdateInput()
    {
        // Run in the CoreWindow.Dispatcher thread so we can update coreWindow.PointerPosition (Windows UWP Mouse.SetPosition workaround)
        var ignored = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
        {
            var coreWindow = CoreApplication.MainView.CoreWindow;

            if (coreWindow != null)
            {
                currentKeyboardState = Keyboard.GetState();
                currentMouseState = Mouse.GetState();
                currentGamePadState = GamePad.GetState(PlayerIndex.One);

                int centerX = Viewport.Width / 2;
                int centerY = Viewport.Height / 2;
                int mouseDeltaX = centerX - currentMouseState.X;
                int mouseDeltaY = centerY - currentMouseState.Y;

                if (currentMouseState.X != previousMouseState.X ||
                    currentMouseState.Y != previousMouseState.Y)
                {
                    if (enableMouseSmoothing)
                    {
                        PerformMouseFiltering((float)mouseDeltaX, (float)mouseDeltaY);
                        PerformMouseSmoothing(smoothedMouseMovement.X, smoothedMouseMovement.Y);
                    }
                    else
                    {
                        smoothedMouseMovement.X = (float)mouseDeltaX;
                        smoothedMouseMovement.Y = (float)mouseDeltaY;
                    }

                    RotateSmoothly(smoothedMouseMovement.X, smoothedMouseMovement.Y);
                }

                if (currentMouseState.X == previousMouseState.X &&
                    currentMouseState.Y == previousMouseState.Y)
                {
                    //Set mouse to center
                    //Mouse.SetPosition(centerX, centerY); // Reference only (Does not work)

                    coreWindow.PointerPosition = new Windows.Foundation.Point(centerX, centerY);
                }

                previousKeyboardState = currentKeyboardState;
                previousMouseState = currentMouseState;

            }
        });

    }
    
    private void PerformMouseFiltering(float x, float y)
    {
        // Shuffle all the entries in the cache.
        // Newer entries at the front. Older entries towards the back.
        for (int i = mouseSmoothingCache.Length - 1; i > 0; --i)
        {
            mouseSmoothingCache[i].X = mouseSmoothingCache[i - 1].X;
            mouseSmoothingCache[i].Y = mouseSmoothingCache[i - 1].Y;
        }

        // Store the current mouse movement entry at the front of cache.
        mouseSmoothingCache[0].X = x;
        mouseSmoothingCache[0].Y = y;

        float averageX = 0.0f;
        float averageY = 0.0f;
        float averageTotal = 0.0f;
        float currentWeight = 1.0f;

        // Filter the mouse movement with the rest of the cache entries.
        // Use a weighted average where newer entries have more effect than
        // older entries (towards the back of the cache).
        for (int i = 0; i < mouseSmoothingCache.Length; ++i)
        {
            averageX += mouseSmoothingCache[i].X * currentWeight;
            averageY += mouseSmoothingCache[i].Y * currentWeight;
            averageTotal += 1.0f * currentWeight;
            currentWeight *= mouseSmoothingSensitivity;
        }

        // Calculate the new smoothed mouse movement.
        smoothedMouseMovement.X = averageX / averageTotal;
        smoothedMouseMovement.Y = averageY / averageTotal;
    }
    
    private void PerformMouseSmoothing(float x, float y)
    {
        mouseMovement[mouseIndex].X = x;
        mouseMovement[mouseIndex].Y = y;

        smoothedMouseMovement.X = (mouseMovement[0].X + mouseMovement[1].X) * 0.5f;
        smoothedMouseMovement.Y = (mouseMovement[0].Y + mouseMovement[1].Y) * 0.5f;

        mouseIndex ^= 1;
        mouseMovement[mouseIndex].X = 0.0f;
        mouseMovement[mouseIndex].Y = 0.0f;
    }
    
    private void RotateSmoothly(float yawDegrees, float pitchDegrees)
    {
        yawDegrees *= rotationSpeed;
        pitchDegrees *= rotationSpeed;

        Rotate(yawDegrees, pitchDegrees);
    }

    public void Rotate(float yawDegrees, float pitchDegrees)
    {
        //yawDegrees = -yawDegrees;
        pitchDegrees = -pitchDegrees;

        accumPitchDegrees += pitchDegrees;

        if (accumPitchDegrees > 90.0f)
            accumPitchDegrees = 90.0f;

        if (accumPitchDegrees < -90.0f)
            accumPitchDegrees = -90.0f;

        accumYawDegrees += yawDegrees;

        if (accumYawDegrees > 360.0f)
            accumYawDegrees -= 360.0f;

        if (accumYawDegrees < -360.0f)
            accumYawDegrees += 360.0f;
        
        float yaw = MathHelper.ToRadians(yawDegrees);
        float pitch = MathHelper.ToRadians(pitchDegrees);
        float roll = 0f;

        LookRotation = Quaternion.CreateFromYawPitchRoll(yaw, pitch, roll);

    }

    public void Rotate_SpinningIssue(float yawDegrees, float pitchDegrees)
    {
        //yawDegrees = -yawDegrees;
        pitchDegrees = -pitchDegrees;

        float yaw = MathHelper.ToRadians(yawDegrees);
        float pitch = MathHelper.ToRadians(pitchDegrees);
        float roll = 0f;

        var test = Quaternion.CreateFromYawPitchRoll(yaw, pitch, roll);
        LookRotation *= test;//???
        //LookRotation = Quaternion.Slerp(LookRotation, test, 1f); // Sorta works but resets
        //LookRotation.Normalize(); // ???
        //LookRotation = Quaternion.Multiply(LookRotation, test);// ???
        //LookRotation += Quaternion.CreateFromYawPitchRoll(yaw, pitch, roll);

        Debug.WriteLine("yawDegrees: " + yawDegrees + ", pitchDegrees: " + pitchDegrees +
            ", accumYawDegrees: " + accumYawDegrees + ", accumPitchDegrees: " + accumPitchDegrees +
            ", yaw: " + yaw + ", pitch: " + pitch);

    }

    private void UpdatePosition(GameTime gameTime)
    {
        // Movement
        if (currentKeyboardState.IsKeyDown(Keys.W) || (currentGamePadState.ThumbSticks.Left.Y > 0))
        {
            // Move forward
            Vector3 rotatedVector = Vector3.Transform(new Vector3(0f, 0f, ForwardSpeed), LookRotation);
            rotatedVector.Y = PlayerPosition.Y; // Stop the player flying
            PlayerPosition += rotatedVector;
        }
        if (currentKeyboardState.IsKeyDown(Keys.S) || (currentGamePadState.ThumbSticks.Left.Y < 0))
        {
            // Move back
            Vector3 rotatedVector = Vector3.Transform(new Vector3(0f, 0f, -ForwardSpeed), LookRotation);
            rotatedVector.Y = PlayerPosition.Y; // Stop the player flying
            PlayerPosition += rotatedVector;
        }
        if (currentKeyboardState.IsKeyDown(Keys.A) || (currentGamePadState.ThumbSticks.Left.X < 0))
        {
            // Strafe left
            Vector3 rotatedVector = Vector3.Transform(new Vector3(StrafeSpeed, 0f, 0f), LookRotation);
            rotatedVector.Y = PlayerPosition.Y; // Stop the player flying
            PlayerPosition += rotatedVector;
        }
        if (currentKeyboardState.IsKeyDown(Keys.D) || (currentGamePadState.ThumbSticks.Left.X > 0))
        {
            // Strafe left
            Vector3 rotatedVector = Vector3.Transform(new Vector3(-StrafeSpeed, 0f, 0f), LookRotation);
            rotatedVector.Y = PlayerPosition.Y; // Stop the player flying
            PlayerPosition += rotatedVector;
        }

    }

    public void Render()
    {
        UpdateCameraFirstPerson();
    }

    private void UpdateCameraFirstPerson()
    {
        // Transform the head offset so the camera is positioned properly relative to the avatar.
        Vector3 headOffset = Vector3.Transform(PlayerHeadOffset, LookRotation);

        // Calculate the camera's current position.
        Vector3 cameraPosition = PlayerPosition + headOffset;

        // Create a vector pointing the direction the camera is facing.
        Vector3 transformedReference = Vector3.Transform(cameraReference, LookRotation);

        // Calculate the position the camera is looking at.
        Vector3 cameraLookat = transformedReference + cameraPosition;

        // Set up the view matrix and projection matrix.
        View = Matrix.CreateLookAt(cameraPosition, cameraLookat, Vector3.Up);
        Projection = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearClip, farClip);

    }

}

If I understand this correctly you have problems with your camera rotation as well as with mouse positioning. My advice would be to not try to solve them both at the same time. First get the camera rotation working properly, then do the mouse stuff. You don’t need the whole mouse-recentering, filtering and smoothing to make a rotating camera. Just use deltaX and deltaY directly, or use keys for now to rotate.

The headOffset rotation doesn’t seem to make sense like that. If you pitch up or down 90 degrees, your head will be on the ground, but I don’t think that’s the main problem.

Vector3 headOffset = Vector3.Transform(PlayerHeadOffset, LookRotation);