Depending on the genre of game that you’re creating, you might want to use a different type of camera, such asa fixed-position camera, a first-person camera, a third-person camera, a realtime strategy (RTS) camera, and so on. With so many different types of cameras, it is helpful to create a basic camera that can be extended to create more specific types of cameras.
The camera’s frustum is defined by the camera’s view and projection matrices,
Camera Perspective Projection
The camera frustum is specified by the viewing angle of the camera and the near and far clipping planes. You’ll create the projection matrix in theSetPerspectiveFovmethod (given this name because the camera’s field of view partly defines the viewing volume). You’ll also define the Projection property, which allows your program to get the projection matrix.
Finally, inside the UpdateProjection method, you generate the new perspective projection matrix using theCreatePerspectiveFieldOfView method of XNA’s Matrix class.
// Perspective projection parameters
float fovy;
float aspectRatio;
float nearPlane;
float farPlane;
// Matrices and flags
protected bool needUpdateProjection;
protected bool needUpdateFrustum;
protected Matrix projectionMatrix;
// Get the camera projection matrix
public Matrix Projection
{
get
{
if (needUpdateProjection) UpdateProjection();
return projectionMatrix;
}
}
// Set the camera perspective projection
public void SetPerspectiveFov(float fovy, float aspectRatio, float nearPlane,
float farPlane)
{
this.fovy = fovy;
this.aspectRatio = aspectRatio;
this.nearPlane = nearPlane;
this.farPlane = farPlane;
needUpdateProjection = true;
}
// Update the camera perspective projection matrix
protected virtual void UpdateProjection()
{
// Create a perspective field of view matrix
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(fovy), aspectRatio, nearPlane, farPlane);
needUpdateProjection = false;
needUpdateFrustum = true;
}
Camera View (Position and Orientation)
A camera is not defined solely by its frustum. You also need to specify where to position your camerain your 3D world, as well ashow to orient it. The camera’s position and orientation in the world are defined by the view matrix created in this section. You’ll create theSetLookAtmethod to set the camera view matrix, and the View property to retrieve it.By allowing your game to retrieve both the view and projection matrix, you make it possible for XNA to transform all 3D objects to your 2D screen.
In order to create a view matrix using the Matrix.CreateLookAt method, you need to know three camera vectors (or directions):the heading (forward) vector, the strafe (right) vector, and the up vector. These vectors uniquely define the orientation of any object in 3D space. The SetLookAt method calculates these three vectors, starting from the camera’s position, its target, and its up vector.
// Position and target
Vector3 position;
Vector3 target;
// Orientation vectors
Vector3 headingVec;
Vector3 strafeVec;
Vector3 upVec;
// Matrices and flags
protected bool needUpdateView;
protected bool needUpdateFrustum;
protected Matrix viewMatrix;
// Get the camera view matrix
public Matrix View
{
get
{
if (needUpdateView) UpdateView();
return viewMatrix;
}
}
// Set the camera view
public void SetLookAt(Vector3 cameraPos, Vector3 cameraTarget, Vector3 cameraUp)
{
this.position = cameraPos;
this.target = cameraTarget;
this.upVec = cameraUp;
// Calculate the camera axes (heading, upVector, and strafeVector)
headingVec = cameraTarget - cameraPos;
headingVec.Normalize();
upVec = cameraUp;
strafeVec = Vector3.Cross(headingVec, upVec);
needUpdateView = true;
}
// Update the camera view
protected virtual void UpdateView()
{
viewMatrix = Matrix.CreateLookAt(position, target, upVec);
needUpdateView = false;
needUpdateFrustum = true;
}
Camera Coordinate SystemEvery time you change the camera’s configuration through the SetLookAt method, you need to calculate the three camera coordinate system vectors: its heading (z axis), strafe (x axis), and up (y axis).
You can calculate the camera vectors as follows:
Heading: The heading vector is the direction from the camera’s position to its target position. It describes the direction the camera is facing. You can calculate this direction by subtracting the camera’s position from its target position.
Up: The up vector defines the camera’s up direction and is used to orient the camera. For example, you can use the vector(0, 1, 0) to orient the camera up vector as the world y axis.
Strafe: The strafe vector is the vector that isperpendicular to the heading and up vectors. This can be found by using the vector cross-product operation, which calculates a vector that is perpendicular to two other vectors at the same time. The Cross method of XNA’s Vector3 class performs a cross-production operation. Note that the vectors used in the cross-product operation must be unitary vectors (or you must normalize the resulting vector after the operation), and the order in which they are passed to the Cross method changes the direction of the resulting vector.
Camera Frustum
Using the XNA BoundingFrustum class, you already have methods to check whether objects are inside the frustum, which will tell you whether they are inside or outside the sight of the camera.
public BoundingFrustum Frustum
{
get
{
if (needUpdateProjection)
UpdateProjection();
if (needUpdateView)
UpdateView();
if (needUpdateFrustum)
UpdateFrustum();
return frustum;
}
}
protected virtual void UpdateFrustum()
{
frustum = new BoundingFrustum(viewMatrix * projectionMatrix);
needUpdateFrustum = false;
}
A Third-Person Camera
In this section, you’ll extend the BaseCamera class from the previous section to create a more specific type of camera: a third-person camera. For this type of camera, you’ll create a class named ThirdPersonCamera, which inherits from the BaseCamera class. The third-person camera’s goal is to follow an object while it moves, and the distance at which the camera follows an object must be variable. Otherwise, it would appear that the object is bound to the camera, resulting in jerky camera movement.
To make the camera follow an object—for example, the player-controlled character—you need to define some parameters, such as the following:
• The chase position, which is the position of the target object the camera must follow
• The chase direction, which is the direction the camera should move to in order to reach the target object
• The chase speed
• The chase distance, which is the distance between the camera and the chase position
// Chase parameters
float desiredChaseDistance;
float minChaseDistance;
float maxChaseDistance;
float chaseSpeed;
private Vector3 chasePosition;
public Vector3 ChasePosition
{
get { return chasePosition; }
set { chasePosition = value; }
}
private Vector3 chaseDirection;
public Vector3 ChaseDirection
{
get { return chaseDirection; }
set { chaseDirection = value; }
}
public void SetChaseParameters(float chaseSpeed,float desiredChaseDistance, float minChaseDistance, float maxChaseDistance)
{
this.chaseSpeed = chaseSpeed;
this.desiredChaseDistance = desiredChaseDistance;
this.minChaseDistance = minChaseDistance;
this.maxChaseDistance = maxChaseDistance;
}
Every time the camera is updated, its position needs to be recalculated. The ideal, new camera position is equal to the camera’s chase position, minus the chase direction, multiplied by the chase distance, as shown in Figure 10-2. The desired new camera position would be the camera’s final position if it were placed at a fixed distance from the chase position. However, to allow the camera to move smoothly, the distance between the camera and the chase position may vary between a minimum and maximum range (defined in the attributes minChaseDistance and maxChaseDistance).
Vector3 targetPosition = chasePosition;
Vector3 desiredCameraPosition = chasePosition –chaseDirection * desiredChaseDistance;
float interpolatedSpeed = MathHelper.Clamp(chaseSpeed * elapsedTimeSeconds, 0.0f, 1.0f);
desiredCameraPosition = Vector3.Lerp(position, desiredCameraPosition,interpolatedSpeed);
private void UpdateFollowPosition(float elapsedTimeSeconds,bool interpolate)
{
Vector3 targetPosition = chasePosition;
Vector3 desiredCameraPosition = chasePosition- chaseDirection *
desiredChaseDistance;
if (interpolate)
{
float interpolatedSpeed = MathHelper.Clamp(
chaseSpeed * elapsedTimeSeconds, 0.0f, 1.0f);
desiredCameraPosition = Vector3.Lerp(position,
desiredCameraPosition, interpolatedSpeed);
// Clamp the min and max follow distances
Vector3 targetVector = desiredCameraPosition - targetPosition;
float targetLength = targetVector.Length();
targetVector /= targetLength;
if (targetLength < minChaseDistance)
{
desiredCameraPosition = targetPosition +
targetVector * minChaseDistance;
}
else if (targetLength > maxChaseDistance)
{
desiredCameraPosition = targetPosition +
targetVector * maxChaseDistance;
}
}
// Needed to recalculate heading, strafe, and up vectors
SetLookAt(desiredCameraPosition, targetPosition, upVec);
}
Another feature you’ll add to the camera is the ability to rotate around its target. For that feature, you’ll want a way to define the maximum rotation speed of the camera and the current rotation of the camera. Also, because you want your camera’s rotation to start and stop smoothly, you’ll need to keep track of the current rotational speed.
// Maximum allowed rotation
public static float MAX_ROTATE = 30.0f;
// Current rotation angles over the camera axes (heading, up, and strafe)
Vector3 eyeRotate;
// Rotation velocity over the camera axes
Vector3 eyeRotateVelocity;
public Vector3 EyeRotateVelocity
{
get { return eyeRotateVelocity; }
set { eyeRotateVelocity = value; }
}
The eyeRotate vector will store the current camera rotation, where the X, Y, and Z components of this vector represent the angle of the rotation around the camera’s strafe, up, and heading axes. Finally, the eyeRotateVelocity vector will store the velocity at which the camera rotation angle is updated.
protected override void UpdateView()
{
Vector3 newPosition = position - target;
// Calculate the new camera position, rotating it around its axes
newPosition = Vector3.Transform(newPosition,
Matrix.CreateFromAxisAngle(upVec, MathHelper.ToRadians(eyeRotate.Y)) *
Matrix.CreateFromAxisAngle(strafeVec, MathHelper.ToRadians(eyeRotate.X)) *
Matrix.CreateFromAxisAngle(headingVec, MathHelper.ToRadians(eyeRotate.Z)));
viewMatrix = Matrix.CreateLookAt(newPosition + target,target, upVec);
needUpdateView = false;
needUpdateFrustum = true;
}
Updating the Camera
You must implement a final method in the ThirdPersonCamera class: the Update method, as it was defined as an abstract method in the BaseCamera class earlier in this chapter. This Update method is called every time the camera needs to be updated. Inside the Update method, you need to update the camera’s attributes, as well as call the methods used to update the camera. The UpdateView and UpdateProjection methods use the camera’s attributes to update the camera’s view and projection matrix. These methods are called only when the view and projection matrices are retrieved through properties and need to be updated.
bool isFirstTimeChase = true;
public override void Update(GameTime time)
{
float elapsedTimeSeconds =
(float)time.ElapsedGameTime.TotalSeconds;
// Update the follow position
UpdateFollowPosition(elapsedTimeSeconds, !isFirstTimeChase);
if (isFirstTimeChase)
{
eyeRotate = Vector3.Zero;
isFirstTimeChase = false;
}
// Calculate the new rotation based on the rotation speed
if (eyeRotateVelocity != Vector3.Zero)
{
eyeRotate += eyeRotateVelocity * elapsedTimeSeconds;
eyeRotate.X = MathHelper.Clamp(eyeRotate.X,
-MAX_ROTATE, MAX_ROTATE);
eyeRotate.Y = MathHelper.Clamp(eyeRotate.Y,
-MAX_ROTATE, MAX_ROTATE);
eyeRotate.Z = MathHelper.Clamp(eyeRotate.Z,
-MAX_ROTATE, MAX_ROTATE);
needUpdateView = true;
}
}