Rotate camera around target

I’m trying to create a RTS style camera where the camera is always looking at a point on the map and also rotates around this point.

If I were to implement this in Unity (which I have some experience with), I would place the camera as a child to some GameObject with the correct distance, and then translate/rotate that GameObject, resulting in the camera moving as well.

So I’m trying to mimick this behaviour here with MonoGame.
I’m positioning my camera using the targets transform (matrix) and an offset vector, which specifies the distance of the camera should follow the target.

var newCameraPosition = target.Position - (target.Forward * offset.Z) + (target.Up * offset.Y) + (target.Right * offset.X);
camera.Position = newPosition;

So in my mind, I should now be able to rotate the target and the camera should always keep the correct distance.

I want the camera to be rotatable by moving the mouse. When the mouse is moved horizontally, the camera should rotate around the Y-axis. When the mouse is moved vertically, the camera should rotate around the X-axis.

When rotating around a single axis, it works as expected.

target.Rotation *= Quaternion.CreateFromAxisAngle(target.Up, mouseDelta.X);

But as soon as I introduce the second axis I get weird behaviour. When I move the mouse horizontally the camera rotates around the Y-axis, but also “bobs” up and down around the X-axis. And vice versa when moving the mouse vertically.

target.Rotation *= (
    Quaternion.CreateFromAxisAngle(target.Up, mouseDelta.X) * 
    Quaternion.CreateFromAxisAngle(target.Right, mouseDelta.Y)
);

I have also tried using Quaternion.CreateFromYawPitchRoll with identical results.

I’m using the transform class from MonoGame.Extended (github)

I’m probably doing something weird or not working with Quaternions correctly.
Any obvious mistakes?

Cannot give hints about you specific problem, but the way I handle such cameras is by maintaining single vectors for each axis of the camera and transforming all of them when rotating around one of them. I don’t need to mix any matrices/quaternions multiplications but apply them one after each other - no problems with multiplication ordering.

Maybe the transform class already does it but applies it different than I would do in my RTS cameras. Axis information is helpful in a lot of other cases of the camera, so it’s the way to go for me

Made this a couple days ago for a quick test.

public class OrbitCamera
{
    public Matrix World = Matrix.Identity;
    public Matrix View { get { return Matrix.Invert(World); } }
    public Matrix Projection = Matrix.Identity;
    private float distanceToTarget = 1.0f;
    public float AddDistanceToTarget { get { return distanceToTarget; } set { if (distanceToTarget + value > 1.0f) distanceToTarget += value; } }

    public OrbitCamera()
    {
        World = Matrix.CreateWorld(new Vector3(0,2, 10), new Vector3(0,0,.1f) - new Vector3(0, 2, 10), Vector3.Up);
        Projection = Matrix.CreatePerspectiveFieldOfView(1.0f, 4f / 3f, 1f, 100000f);
    }
    public Vector3 Position
    {
        set { World.Translation = value; }
        get { return World.Translation; }
    }
    public Vector3 Forward
    {
        set { World = Matrix.CreateWorld(World.Translation, Vector3.Normalize(value), Vector3.Up); }
        get { return World.Forward; }
    }
    public void SetTargetAsPosition(Vector3 targetPosition)
    {
        World = Matrix.CreateWorld(World.Translation, targetPosition - World.Translation, Vector3.Up);
    }
    
    public void OrbitTargetLeftRight(Vector3 targetPosition, float distance, float angle)
    {
        var rotAmnt = Matrix.CreateRotationY(angle);
        World *= rotAmnt;
        World.Translation = Vector3.Normalize(World.Translation - targetPosition) * distance;
        World = Matrix.CreateWorld(World.Translation, targetPosition - World.Translation, World.Up);
    }
    public void OrbitTargetUpDown(Vector3 targetPosition, float distance, float angle)
    {
        var rotAmnt = Matrix.CreateRotationX(angle);
        World *= rotAmnt;
        World.Translation = Vector3.Normalize(World.Translation - targetPosition) * distance;
        World = Matrix.CreateWorld(World.Translation, targetPosition - World.Translation, World.Up);
    }
    public void MoveTowardsAway(Vector3 targetPosition, float distance)
    {
        World.Translation = Vector3.Normalize(World.Translation - targetPosition) * distance;
        World = Matrix.CreateWorld(World.Translation, targetPosition - World.Translation, World.Up);
    }
}

some test code maybe that will help.

        float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
        float speedRotationPerSecond = 0.8f;
        float speedMovementPerSecond = 2.0f;
        if (Keyboard.GetState().IsKeyDown(Keys.A))
            cam.OrbitTargetLeftRight(targetMesh.Position, cam.AddDistanceToTarget, -speedRotationPerSecond * elapsed);
        if (Keyboard.GetState().IsKeyDown(Keys.D))
            cam.OrbitTargetLeftRight(targetMesh.Position, cam.AddDistanceToTarget, +speedRotationPerSecond * elapsed);
        if (Keyboard.GetState().IsKeyDown(Keys.W))
            cam.OrbitTargetUpDown(targetMesh.Position, cam.AddDistanceToTarget, -speedRotationPerSecond * elapsed);
        if (Keyboard.GetState().IsKeyDown(Keys.S))
            cam.OrbitTargetUpDown(targetMesh.Position, cam.AddDistanceToTarget, +speedRotationPerSecond * elapsed);
        if (Keyboard.GetState().IsKeyDown(Keys.E))
        {
            cam.AddDistanceToTarget = -speedMovementPerSecond * elapsed;
            cam.OrbitTargetMoveTowardsAway(targetMesh.Position, cam.AddDistanceToTarget);
        }
        if (Keyboard.GetState().IsKeyDown(Keys.Q))
        {
            cam.AddDistanceToTarget = +speedMovementPerSecond * elapsed;
            cam.OrbitTargetMoveTowardsAway(targetMesh.Position, cam.AddDistanceToTarget);
        }

Awesome thanks! I have not had time to try it yet, but it looks good. I’ll answer back with my results.