index to lectures
In this chapter, we'll implement a first-person camera and discuss issues related to moving that camera in 3D space (as in a flight simulator) versus moving a land-based camera.
The view matrix defines where the camera sits in the world and how it is rotated. The view matrix is created from three vectors: the camera's position, the point the camera is looking at, and a vector indicating which direction is up for the camera. In contrast to the projection matrix, which does not need to change when a camera moves, the view matrix will constantly change to reflect new rotations and positions of the camera.
This means two important things in relation to your cameraDirection vector. First, instead of passing cameraDirection as the second parameter of your Matrix.CreateLookAt method, you'll need to pass cameraPosition + cameraDirection. Second, your cameraDirection vector cannot be (0, 0, 0) the variable must contain a value representing something other than the origin because it represents the direction in which your camera is looking, and the vector (0, 0, 0) has no direction.
Key: Direction of Movement Think about it - when you walk forward, what direction do you typically move in? Usually, you walk in the direction that you are facing. That is true of most game cameras as well. There are a few exceptions that allow the player to look in one direction and move in another, but as a general rule, you move a camera forward in the direction in which it faces.
What does that mean to you? Well, because you already have a vector that represents the direction in which the camera is facing, you can use that vector to move your camera forward. As you'll see shortly, you can move a camera forward by simply adding the direction vector to the position vector. This moves the position vector in the direction of the direction vector, and in turn moves the camera forward in the direction in which it's facing.
So, why normalize the direction vector? Remember that normalizing the vector will make it have a length or magnitude of one. Dealing with a vector with a length of one makes it much easier to apply things such as different speed values to the movement of the camera.
float speed = 3;
Compile and run the game at this point, and you'll see that when you press the W and S keys, the camera moves closer to and farther from the spaceship. It's important to note what's happening here: in our previous 3D examples, you've moved objects around by changing the world matrix for those objects, but in this example, instead of moving the object, you're moving the camera itself.
Now that you can move your camera forward and backward, you'll want to add other movement features. Any good 3D first-person camera also has strafing (or side-to-side movement) support. Your camera vectors are position, direction, and up, so how can you find a vector that will move your camera sideways? We may be able to solve this problem with a little bit of vector math. Think about what you need in order to move sideways: you'll need a vector that points in the direction that is sideways-on to your camera. If you had that vector, moving sideways would be just as easy as moving forward—you could simply add the sideways vector to the camera's position vector. As shown in Figure 11-2, you currently have a vector for the camera's up direction as well as a vector for the direction in which the camera is facing, but you don't have a vector representing the direction that is sideways-on to the camera.
Here's where the vector math can help: a cross product is a binary operation performed on two vectors in 3D space that results in another vector that is perpendicular to the two input vectors. Therefore, by taking the cross product of the up and direction vectors of your camera, you will end up with a vector perpendicular to those two vectors (i.e., coming from the side of your camera). This is illustrated in Figure 11-3. The cross product of your negative up and direction vectors will yield a perpendicular vector coming from the other direction (sideways on to the other side of the camera).
Tip: It's not critical that you understand how this vector math works, but if you're curious, feel free to investigate in some math textbooks. All you need to understand for now is that it does indeed work: the cross product of any two vectors yields a third vector perpendicular to the other two, and the cross product of the up and direction vectors of your camera yields a vector indicating the sideways direction for your camera.
It may help to picture a person standing and looking forward. The person's direction vector would be pointing straight ahead, while his up vector would be pointing straight up. The cross product of the up and direction vectors would essentially be the direction the person's left arm would be pointing in if it were held out perpendicular to both his up and direction vectors. The only vector that fits that criterion would be one that was pointing directly outward from the person's side.
XNA provides a method that will create a vector based on the cross product of two other vectors. It's a static method in the Vector3 class called Cross. Pass in any two vectors to the Cross method, and the resulting vector will be the cross product of the two vectors passed in.
To enable the camera to move from side to side using the A and D keys, insert the following code immediately after the code you just added, which moves the camera forward and backward:
// Move side to side if (Keyboard.GetState( ).IsKeyDown(Keys.A)) cameraPosition += Vector3.Cross(cameraUp, cameraDirection) * speed; if (Keyboard.GetState( ).IsKeyDown(Keys.D)) cameraPosition -= Vector3.Cross(cameraUp, cameraDirection) * speed;
In the classes in which I've taught XNA, one of the things that has traditionally been difficult for some students to understand is the fact that yaw, pitch, and roll rotations when applied to objects or cameras that move and rotate in 3D don't necessarily correspond to rotations around the X-, Y-, and Z-axes.
For example, picture the camera you currently have in your game. The camera sits on the Z-axis and faces in the negative Z direction. If you wanted to rotate the camera in a roll, you could rotate the camera around the Z-axis. However, what would happen if the camera rotated to turn 90° and was now looking in the direction of positive X? If you performed a rotation around the Z-axis at this point, you'd rotate in a pitch rather than a roll.
It's easier to think of yaw, pitch, and roll as related to the vectors available to you in your camera. For example, a yaw, rather than rotating around the Y-axis, rotates around the camera's up vector. Similarly, a roll rotates around the camera's direction vector, and a pitch rotates around a vector coming out of the side of the object, perpendicular to the up and direction vectors. Any idea how you'd get that perpendicular vector? That's right, you've used it before to add strafing ability: it's the cross product of the up and direction vectors. Figure 11-5 illustrates how yaw, pitch, and roll rotations are accomplished in a 3D camera.
Which of these rotations you choose to implement in your particular game completely depends on what type of experience you want to give the player. For example, a typical space simulator will have the ability to yaw, pitch, and roll in an unlimited fashion. A helicopter simulator may allow yaw, pitch, and roll to some extent, but might not allow you to perform a complete roll (a fairly difficult task in a helicopter). A land-based shooter may only allow a yaw and a pitch, though some games allow roll rotations for special moves like tilting your head to look around a corner.
Once you've decided which of these rotations you'll allow in your camera, the next step is to implement them. Each of these camera rotations can be accomplished by rotating one or more of your camera's vectors. For a yaw, pitch, or roll, it helps to evaluate the rotation using these steps: first, determine which of the three camera vectors need to rotate; second, figure out what axis you will need to rotate those vectors around; and third, determine which methods will be needed to accomplish this.
The axis you want to rotate the direction vector around for a yaw is the camera's up vector. The method used to rotate a Vector3 is Vector3.Transform, which takes two parameters: the source or original vector, and a matrix representing a rotation or translation to apply to the vector.
Tip: When performing a yaw rotation for a camera, why rotate around the camera's up vector instead of the Y-axis?The Y-axis may not always be up for a camera. It might be for a land-based shooter game, but consider a flight simulator that freely flies and rotates in three dimensions. In that case, you'd always want to yaw around the up vector of the camera.
Before you add the Vector3.Transform to perform the yaw, you'll want to add some code to allow your camera to capture mouse movement. A typical first-person configuration uses the WASD keys for movement and the mouse for rotating the camera. So, to capture mouse movement, add the following class-level variable to your Camera class:
MouseState prevMouseState;Next, in the Initialize method of your Camera class, set the initial position of the mouse cursor to the middle of the screen. Also, add the code to initialize the new variable:
// Set mouse position and do initial get state Mouse.SetPosition(Game.Window.ClientBounds.Width / 2, Game.Window.ClientBounds.Height / 2); prevMouseState = Mouse.GetState( );Remember that the Mouse.GetState call returns the mouse position. To find out how far the mouse has actually moved, you need to capture the state from the previous frame and compare it to the current state in each frame. You initialize the state variable in the Initialize method so that you have something to compare against in the first frame (the first time Update is called).
Now you're ready to code your yaw rotation. In the Update method of your Camera class, add the following code just above the call to CreateLookAt:
// Yaw rotation cameraDirection = Vector3.Transform(cameraDirection, Matrix.CreateFromAxisAngle(cameraUp, (-MathHelper.PiOver4 / 150) * (Mouse.GetState( ).X - prevMouseState.X))); // Reset prevMouseState prevMouseState = Mouse.GetState( );In this code, you're assigning the cameraDirection vector the value given in the Vector3.Transform call. By passing in cameraDirection as the first parameter, you ensure that the Vector3.Transform method will apply the rotation specified in the second parameter to the cameraDirection and return the resulting vector. The matrix specified in the second parameter is created from the CreateFromAxisAngle method, which creates a rotation around a specific axis (in this case, the camera's up vector). The angle of rotation is determined by how much the mouse has moved horizontally.
Compile and run the game at this point, and you'll see that not only can you move in 3D space, but you can now yaw the camera left and right. It may seem a bit awkward because you don't have full rotation of your camera yet, but that will come shortly.
Tip: If your camera moves backward relative to your mouse (i.e., if you move the mouse right and it rotates the camera left), you've probably left off the negative sign in front of the MathHelper.PiOver4 in the code. Add that and it should work properly.
In a roll, the only camera vector that changes is the camera's up vector. The vector that you want to rotate your camera's up vector around is the camera's direction vector. Add the following code to the Update method of your Camera class, just before the prevMouseState = Mouse.GetState( ) line:
// Roll rotation if (Mouse.GetState( ).LeftButton == ButtonState.Pressed) { cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromAxisAngle(cameraDirection, MathHelper.PiOver4 / 45)); } if (Mouse.GetState( ).RightButton == ButtonState.Pressed) { cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromAxisAngle(cameraDirection, -MathHelper.PiOver4 / 45)); }Run the game now, and you'll see that with the left and right mouse buttons you can roll your camera left or right. It will probably look a little strange in this example because the only thing that you're drawing is the spaceship and it is rotating as well, which makes the rotation of your camera seem off.
Let's make the ship not spin anymore, so you can get a better sense of how your camera is working. In the ModelManager's LoadContent method, change the type of ship that's being created from a SpinningEnemy to a BasicModel:
models.Add(new BasicModel( Game.Content.Load(@"models\spaceship")));
This is one place where you'll need to stop and think about what kind of functionality you want in your camera. Typically, in a flight simulator, you'd rotate both your direction and your up vector in a pitch. The reason? Remember that in a yaw you rotate around your up vector. In a flight simulator, you'll want to have your up vector change in a roll and a pitch to make your yaw rotation more realistic.
What about pitching in a land-based shooter? Would you want to rotate your up vector in a pitch in that scenario? Again, remember that when you yaw, you do so around the up vector. Imagine hunting down an enemy and looking two or three stories up a wall to see if he's perched on a ledge. Then, you rotate in a yaw to scan the rest of that level of the building. You'd expect your rotation in that case to be based on the Y-axis—rotating around the up vector (if it was changed by your pitch) would cause an unexpected rotation.
One solution to this is to use the up vector for rotating in a yaw with a flight simulator and use the Y-axis for yaw rotations with land-based cameras. However, there's another thing to consider here: typically in a land-based shooter you can't pitch a full 360°. When looking up, you typically, can't look straight up; you can pitch your camera until it is maybe 10–15° away from exactly up, but no further. One reason for this is that in XNA, if the angle between your up vector and your direction vector is small enough, XNA doesn't know how to draw what you're telling it to draw, and it will freak out on you a little bit. To avoid this, it's common to set a limit on how far you can pitch your camera. But if you're going to set a limit on how much you can pitch, you might as well just not rotate your up vector in a pitch on a game like this.
Either way, these are some things to think about. In this example, you're going to use a flight simulator approach, so you'll be rotating both the up and the direction vectors. Now that you know what you're going to rotate, you need to figure out what to axis rotate around. The pitch rotates around a vector that runs out from the side of your camera. Remember using Vector3.Cross to get a vector perpendicular to the up and direction vectors of your camera when strafing? You'll be using that same vector to rotate your direction and up vectors in a pitch.
In the Update method of the Camera class, add the following code just before the prevMouseState = Mouse.GetState( ) line:
// Pitch rotation cameraDirection = Vector3.Transform(cameraDirection, Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection), (MathHelper.PiOver4 / 100) * (Mouse.GetState( ).Y - prevMouseState.Y))); cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection), (MathHelper.PiOver4 / 100) * (Mouse.GetState( ).Y - prevMouseState.Y)));Your camera now has full movement and full yaw, pitch, and roll rotation. Run the game and fly around the world to see different angles of the ship that weren't previously available, as shown in Figure 11-6.
If you download the source code for this chapter, you'll find the free-flying camera code in the folder called Flying Camera. The code that is used through the rest of this chapter is located with the source code as well, in the folder called 3D Game.
The game that you're going to build in the rest of this book will use a stationary camera that can rotate a total of 45° in a pitch and 45° in a yaw. Later, you'll be adding some code to send ships flying toward the camera, which you'll have to shoot down.
Because you won't be moving your camera and you won't be rotating in a roll, you can go into the Camera class's Update method and remove the code that enables that functionality.
To do this, remove the following code (which moves the camera forward/backward and side to side) from the Update method of the Camera class:
// Move forward/backward if (Keyboard.GetState( ).IsKeyDown(Keys.W)) cameraPosition += cameraDirection * speed; if (Keyboard.GetState( ).IsKeyDown(Keys.S)) cameraPosition -= cameraDirection * speed; // Move side to side if (Keyboard.GetState( ).IsKeyDown(Keys.A)) cameraPosition += Vector3.Cross(cameraUp, cameraDirection) * speed; if (Keyboard.GetState( ).IsKeyDown(Keys.D)) cameraPosition -= Vector3.Cross(cameraUp, cameraDirection) * speed;Also remove this code, which rolls the camera (also located in the Update method of the Camera class):
// Roll rotation if (Mouse.GetState( ).LeftButton == ButtonState.Pressed) { cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromAxisAngle(cameraDirection, MathHelper.PiOver4 / 45)); } if (Mouse.GetState( ).RightButton == ButtonState.Pressed) { cameraUp = Vector3.Transform(cameraUp, Matrix.CreateFromAxisAngle(cameraDirection, -MathHelper.PiOver4 / 45)); }