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:
Translate the Camera world matrix to the Player world matrix translation
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
Translate the Camera world backwards and up so that the camera is a good distance from and above the player
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.
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:
@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
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.
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;