(Solved) Strange Tilt When Camera Orbits Object

I’m currently in the early stages of development of a 3rd person camera. I designed the camera to passively try to stay above and behind the player, such that if the player changes direction, the camera rotates at a fixed rate until it is behind the player once more. The camera works as expected, but there appears to be a slight offset, as thought it is tilted somehow when rotating. I’m having a hard time identifying the nature of the issue due to how minor its effect is, but I’m hoping that someone with more experience manipulating cameras in MonoGame may be able to spot an issue with my logic. In brief, the camera updates each frame after all objects, using the following steps:

  1. Translate the Camera world matrix to the Player world matrix translation
  2. find the difference in yaw between the Camera world and the Player world
    2a. If the difference is within the Camera’s step rotation amount, adopt the Player world forward Vector
    2b. Otherwise, apply a Y rotation to the Camera world by the step rotation amount
  3. Translate the Camera world backwards and up so that the camera is a good distance from and above the player
  4. set the Camera view matrix to CreateLookAt from Camera world to Player world

The code in its entirety can be found below:

        /// <summary>
	/// passively try to keep the camera behind the player with a fixed maximum rotation change over time
	/// </summary>
	/// <param name="gameTime">the elapsed time since the last update call</param>
	void passiveBehind(GameTime gameTime) {
		Matrix playerWorld = GameManager.player.world;

		//start by moving the world matrix to the player's location
		world.Translation = playerWorld.Translation;

		if (world.Forward != playerWorld.Forward) {
			//grab the camera yaw and player yaw
			float yawCam, yawPlayer, yawTest, pitch, roll;
			Util.ExtractYawPitchRoll(world, out yawCam, out pitch, out roll);
			Util.ExtractYawPitchRoll(playerWorld, out yawPlayer, out pitch, out roll);

			//find the change in yaw between the camera and player
			float diff = Util.angleDiff(yawCam, yawPlayer);
			float angleChange = (float)gameTime.ElapsedGameTime.TotalSeconds * passiveRotSpeed;
			
			//if the change is within our maximum change this frame, simply adopt the player's forward angle
			if (Math.Abs(diff) <= angleChange) {
				world.Forward = playerWorld.Forward;
			}
			else {
				//rotate by +-angleChange, depending on which direction reduces the remaining angle difference
				Matrix testMat = world * Matrix.CreateRotationY(angleChange);
				Util.ExtractYawPitchRoll(testMat, out yawTest, out pitch, out roll);
				float testDiff = Util.angleDiff(yawTest, yawPlayer);
				if (testDiff < diff) {
					world = testMat;
				}
				else {
					world *= Matrix.CreateRotationY(-angleChange);
				}
			}
		}

		//with our rotated angle, move back away from the player
		world.Translation += (world.Backward * camDistance);
		world.Translation += (playerWorld.Up * camHeight);
		view = Matrix.CreateLookAt(world.Translation, playerWorld.Translation, Vector3.Up);
	}

Any help would be greatly appreciated! If the issue is not apparent from this code, I would be happy to upload the project in its entirety.

Aint this related to a sort of glimbal lock problem. After many rotations and rounding errors, the horizon seems no more horizontal?

Does swapping the matrix multiplication help?

Matrix testMat = Matrix.CreateRotationY(angleChange) * world;

An alternative, and maybe simpler, approach would be to just calculate the destination position for the camera, and then interpolate the camera from it’s current position towards that destination. Not sure if that works for you, but something like that in pseudocode:

camDestination = playerPos + player.Backwards * camDistance + up * camHeight;
camPos = Interpolate(camPos, camDestination, deltaTime);
view = CreateLookAt(camPos, playerPos);

You can use different interpolation methods for Interpolate(), a simple one could be:

camPos = camPos + (camDestination - camPos) * deltaTime * camSpeed;

2 Likes

You update the player before the camera, right?

@Alkher That’s a great thought. While I considered the possibility that this may be a result of gimbal locking, I don’t think this is the case since the issue can be triggered immediately by applying only a single rotation to the player. That said, I’m still pretty terrible at conceptualizing matrix math, so I’m sure it is a possibility.

@nkast Yup, that was the first thing I checked, just in case haha

@markus That fixed it!!

Thanks so much everyone! Changing the matrix multiplication order as markus suggested did the trick:

Matrix testMat = Matrix.CreateRotationY(angleChange) * world;
...
world = Matrix.CreateRotationY(-angleChange) * world;

I’m going to go ahead and mark this thread as ‘Solved’, although I would be very interested if anyone could explain why the matrix multiplication order was the issue here.

If your world matrix didn’t have a translation (translation=0), the order shouldn’t matter. You would always get the combined rotation. Since you have a translation it matters.

rotation * world basically means that you start with the rotation matrix, and then you apply the rotation and translation from the world matrix to it. This will combine the two rotations, and the resulting translation will be the original translation of the world. That’s what you want.

world * rotation means that you start with the world matrix, and then you apply the rotation from the rotation matrix to it. That will also combine the rotations, but it will also rotate the world’s translation around the zero point (world center). So the final translation is not the same as the original world translation.

1 Like

Well i just wanted to chime in.

you can do it the second way as well using vector3.transform on the camera’s world.Translation , rotate it and put it back in i do it with my camera.

This is can done by using the Vector3.Transform(… , …) function.

The matrix to pass in is a rotation matrix directly from Matrix.CreateRotationY( some radian amount of angular motion )

The vector passed is (var v =) the cameras.world.translation this is the m41 m42 m43 (xyz) positional portion of the orientation matrix.

Then in the CreateLookAt function you
use the returned value as the position.
use the player.World.Translation (i.e. its position) as the target
use the player.World.Up as the up vector.
Alternately you can use vector3.Up as the up.

What you see with the upset tilt can happen for other reasons.
The quick fix in any case, Is to manually re-normalize the cameras world matrix. (this is also sort of how you fix gimble lock but that deserves a little more explanation)
Take the targets up (typically the players up) cross it with the cameras forward (the forward is usually found proper like so normalized(Target.Position - Camera.Position)). Then… Stick that result ( a Right or axis y matrix row result) back into the cameras right
The targets up goes into the cameras up
the normalized forward goes into the cameras forward.
Now the camera world matrix is re-normalized.
The create lookat function will work properly.

Also gimble lock occurs due to 2 normals having the same values negative or positive when entered into a cross product calculation which occurs in the look at function. Doing the math and with the explanation you can imagine how to prevent that with a few if else statements;

1 Like