First-Person Camera
It was handsome at the auction, oh but when we got it home,
it grew into something we could no longer contain
Where's our First-Person camera, by now he could be anywhere and after all that training.
- The Tragically Hip, First Person Camera
We know from the previous tutorials that we can treat our View Matrix like a camera. This isn't the most efficient
solution because every time you change the View Matrix D3D has to make a number of internal adjustments. But it is a
quick and easy way to get things going, so that's what we'll do. Optimizations aren't the goal of this tutorial,
concepts are. And these same concepts apply to the more optimized methods as well.
When we create our View Matrix we have 3 vectors:Eye (position), LookAt (facing), Up. In a standard FPS your Up
vector won't often change. However if you allow complete 3D movement (Descent) or allow leaning (System Shock 2)
then it would come in to play. For this lesson we won't be doing any of that, so our Up vector will never change
from its initial setting (0,1,0).
The basic issues in handling a First-Person camera are facing and position. Facing is allowing
the user to spin around and look at the world around him, while position is calculating the direction and velocity
of the camera to find out where we are in 3D space. Our position calculations require that we have our
facing sorted out, so we'll do that first.
Facing
Let's have a look at our player (The relevant bits are highlighted):
struct player_struct{
D3DXVECTOR3 position;
D3DXVECTOR3 up;
float angle;
} g_player;
As the comments say, angle is our rotation on the Y-axis. Note:In this lesson we only allow the player to look
right or left, looking up and down is not supported. That's why we can represent our facing with just a single
rotation angle.
There are a lot of different ways you can organize your data. I could have easily chosen to
use a facing vector. I could have chosen to use a completely different representation like a quaternion.
Using the angle was a choice driven by convenience. It worked well for me.
You may have noticed that I specified that the angle is in radians. Radians are the units used for all of the trig
functions (some of which we'll be using shortly) so it just makes sense to store them in the way they'll be used. Some
people are hesitant to move away from degrees because degrees are familiar. But they really aren't difficult to use.
Most of the time you deal with
angles, you'll be dealing with them relatively anyway. When the player turns right, we'll just increment the angle a wee
bit, the fact that it's in radians isn't really an issue. For those rare times when you need absolutes, they're easy to
remember. 180 degrees is PI radians. PI is roughly equal to 3.14159 and is represented by π,
which is the Greek symbol for
"ottoman".
360 degrees is twice as big as 180, and so it's 2π. That's about all there is to that.
Now let's look back at our code to see how we handle the player hitting a turn key.
if(g_key_state[KEY_RIGHT]){
g_player.angle+=turn_rate;
if(g_player.angle > pi2){
g_player.angle-=pi2;
}
changed_camera=true;
}else if(g_key_state[KEY_LEFT]){
g_player.angle-=turn_rate;
if(g_player.angle < 0){
g_player.angle+=pi2;
}
changed_camera=true;
}
If the player hits the right or left arrows keys we increment/decrement the angle. We also make sure the angle stays
within the range 0..2π. If the angle became very large in magnitude (postive or negative), error could
be introduced and our calculations would be off.
We set our changed_camera variable to true so we know to rebuild our view matrix. As I mentioned above, this is an expensive
operation so we only do it when needed.
Storing our facing as an angle was very convenient here. Doing the same thing with a vector would have been more complex.
Now that we've calculated our new facing we need to update our look-at point. The function we use to build our view
matrix (D3DXMatrixLookAtLH) takes vectors as parameters, not rotation angles. That means we have to convert our angle
to a vector that it wants, which is a look-at point. Here's that little bit of magic:
look_at.x=sinf(g_player.angle)+g_player.position.x;
look_at.y=g_player.position.y;
look_at.z=cosf(g_player.angle)+g_player.position.z;
Since the y coordinate of our look-at point is the same as our players height, no calculation is required. The x and
z coordinated require a little bit of trig. The Sine of the angle gives us our x, and we add the player position to make
sure the look_at is ahead of our player. Similarly, the z coordinate comes from the Cosine of our angle. And that's
all there is to handling our facing.
Movement
Updating our position isn't much harder than updating our facing. In fact it's very similar. To calculate our look-at
point on the x-axis, we found the Sine of the player angle and added it to our position on the a-axis. Similarly, to
find our velocity on the x-axis, we multiply the Sine of the angle by our base velocity. This gives us our velocity along
the x-axis. If we're moving forward, we add the result to our position. If we're moving backwards, we subtract it.
We do the same for the z-axis and we're done.
if(g_key_state[KEY_UP]){
g_player.position.x+=(velocity) * (sinf(g_player.angle));
g_player.position.z+=(velocity) * (cosf(g_player.angle));
changed_camera=true;
}else if(g_key_state[KEY_DOWN]){
g_player.position.x-=(velocity) * (sinf(g_player.angle));
g_player.position.z-=(velocity) * (cosf(g_player.angle));
changed_camera=true;
}
Building Our View
As I mentioned earlier, the up vector never changes in this lesson, so our player's up vector is static. We just
pass it in without update. The position vector is calculated every time we move forward or backward. Our look-at point
updates whenever we turn. Since we've calculated our new vectors in the above sections, all we have to do is rebuild
the view matrix and apply it.
D3DXMatrixLookAtLH(&view_matrix,
&g_player.position,
&look_at,
&g_player.up);
g_engine.SetTransform(D3DTS_VIEW,&view_matrix);
And that is all there is to that. You are now the proud owner of a first-person camera.
|