Chapter 9

Cameras

The camera determines the player’s point of view in a 3D game world, and there are many different types of cameras. This chapter covers the implementation of four cameras: a first-person camera, a follow camera, an orbit camera, and a spline camera that follows paths. And because the camera often dictates the movement of the player character, this chapter also covers how to update movement code for different types of cameras.

First-Person Camera

A first-person camera shows the game world from the perspective of a character moving through the world. This type of camera is popular in first-person shooters such as Overwatch but also sees use in some role-playing games like Skyrim or narrative-based games such as Gone Home. Some designers feel that a first-person camera is the most immersive type of camera for a video game.

Even though it’s tempting to think of a camera as just a view, the camera also informs the player how the player character moves around the world. This means the camera and movement system implementations depend on each other. The typical controls for a first-person shooter on PC use both the keyboard and mouse. The W/S keys move forward and backward, while the A/D keys strafe the character (that is, move left and right). Moving the mouse left and right rotates the character about the up axis, but moving the mouse up and down pitches only the view, not the character.

Basic First-Person Movement

Implementing movement is easier than working with the view, so this is a good starting point. You create a new actor called FPSActor that implements first-person movement. The forward/back movement in MoveComponent already works in the 3D world, based on the changes made in Chapter 6, “3D Graphics.” Implementing strafing requires just a few updates. First, you create a GetRight function in Actor, which is like GetForward (just using the y-axis instead):

Vector3 Actor::GetRight() const
{
   // Rotate right axis using quaternion rotation
   return Vector3::Transform(Vector3::UnitY, mRotation);
}

Next, you add a new variable in MoveComponent called mStrafeSpeed that affects the speed at which the character strafes. In Update, you simply use the right vector of the actor to adjust the position based on the strafe speed:

if (!Math::NearZero(mForwardSpeed) || !Math::NearZero(mStrafeSpeed))
{
   Vector3 pos = mOwner->GetPosition();
   pos += mOwner->GetForward() * mForwardSpeed * deltaTime;
   // Update position based on strafe
   pos += mOwner->GetRight() * mStrafeSpeed * deltaTime;
   mOwner->SetPosition(pos);
}

Then in FPSActor::ActorInput, you can detect the A/D keys and adjust the strafe speed as needed. Now the character can move with standard first-person WASD controls.

The left/right rotation also already exists in MoveComponent via the angular speed. So, the next task is to convert mouse left/right movements to angular speed. First, the game needs to enable relative mouse mode via SDL_RelativeMouseMode. Recall from Chapter 8, “Input Systems,” that relative mouse mode reports the change in (x, y) values per frame, as opposed to absolute (x, y) coordinates. (Note that in this chapter, you will directly use SDL input functions rather than the input system created in Chapter 8.)

Converting the relative x movement into an angular speed only requires a few calculations, shown in Listing 9.1. First, SDL_GetRelativeMouseState retrieves the (x, y) motion. The maxMouseSpeed constant is an expected maximum amount of relative motion possible per frame, though this might be an in-game setting. Similarly, maxAngularSpeed converts the motion into a rotation per second. You then take the reported x value, divide by maxMouseSpeed, and multiply by maxAngularSpeed. This yields an angular speed that’s sent to the MoveComponent.

Listing 9.1 FPS Angular Speed Calculation from the Mouse


// Get relative movement from SDL
int x, y;
Uint32 buttons = SDL_GetRelativeMouseState(&x, &y);
// Assume mouse movement is usually between -500 and +500
const int maxMouseSpeed = 500;
// Rotation/sec at maximum speed
const float maxAngularSpeed = Math::Pi * 8;
float angularSpeed = 0.0f;
if (x != 0)
{
   // Convert to approximately [-1.0, 1.0]
   angularSpeed = static_cast<float>(x) / maxMouseSpeed;
   // Multiply by rotation/sec
   angularSpeed *= maxAngularSpeed;
}
mMoveComp->SetAngularSpeed(angularSpeed);


Camera (Without Pitch)

The first step to implement a camera is to create a subclass of Component called CameraComponent. All the different types of cameras in this chapter will subclass from CameraComponent, so any common camera functionality can go in this new component. The declaration of CameraComponent is like that of any other component subclass. For now, the only new function is a protected function called SetViewMatrix, which simply forwards the view matrix to the renderer and audio system:

void CameraComponent::SetViewMatrix(const Matrix4& view)
{
   // Pass view matrix to renderer and audio system
   Game* game = mOwner->GetGame();
   game->GetRenderer()->SetViewMatrix(view);
   game->GetAudioSystem()->SetListener(view);
}

For the FPS camera specifically, you create a subclass of CameraComponent called FPSCamera, which has an overridden Update function. Listing 9.2 shows the code for Update. For now, Update uses the same logic as the basic camera actor introduced in Chapter 6. The camera position is the owning actor’s position, the target point is an arbitrary point in the forward direction of the owning actor, and the up vector is the z-axis. Finally, Matrix4::CreateLookAt creates the view matrix.

Listing 9.2 FPSCamera::Update Implementation (Without Pitch)


void FPSCamera::Update(float deltaTime)
{
   // Camera position is owner position
   Vector3 cameraPos = mOwner->GetPosition();
   // Target position 100 units in front of owner
   Vector3 target = cameraPos + mOwner->GetForward() * 100.0f;
   // Up is just unit z
   Vector3 up = Vector3::UnitZ;
   // Create look at matrix, set as view
   Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, up);
   SetViewMatrix(view);
}


Adding Pitch

Recall from Chapter 6 that yaw is rotation about the up axis and pitch is rotation about the side axis (in this case, the right axis). Incorporating pitch into the FPS camera requires a few changes. The camera still starts with the forward vector from the owner, but you apply an additional rotation to account for the pitch. Then, you derive a target from this view forward. To implement this, you add three new member variables to FPSCamera:

// Rotation/sec speed of pitch
float mPitchSpeed;
// Maximum pitch deviation from forward
float mMaxPitch;
// Current pitch
float mPitch;

The mPitch variable represents the current (absolute) pitch of the camera, while mPitchSpeed is the current rotation/second in the pitch direction. Finally, the mMaxPitch variable is the maximum the pitch can deviate from the forward vector in either direction. Most first-person games limit the total amount the player can pitch the view up or down. The reason for this limitation is that the controls seem odd if the player faces straight up. In this case, you can use 60° (converted to radians) as the default maximum pitch value.

Next, you modify FPSCamera::Update to take into account the pitch, as in Listing 9.3. First, the current pitch value updates based on the pitch speed and delta time. Second, you clamp the pitch to make sure it does not exceed +/- the maximum pitch. Recall from Chapter 6 that a quaternion can represent an arbitrary rotation. Thus, you can construct a quaternion representing this pitch. Note that this rotation is about the owner’s right axis. (It’s not just the y-axis because the pitch axis changes depending on the owner’s yaw.)

The view forward is then the owner’s forward vector, transformed by the pitch quaternion. You use this view forward to determine the target position that’s “in front” of the camera. You also rotate the up vector by the pitch quaternion. Then you construct the look-at matrix from these vectors. The camera position is still the owner’s position.

Listing 9.3 FPSCamera::Update Implementation (with Pitch Added)


void FPSCamera::Update(float deltaTime)
{
   // Call parent update (doesn't do anything right now)
   CameraComponent::Update(deltaTime);
   // Camera position is owner position
   Vector3 cameraPos = mOwner->GetPosition();
   
   // Update pitch based on pitch speed
   mPitch += mPitchSpeed * deltaTime;
   // Clamp pitch to [-max, +max]
   mPitch = Math::Clamp(mPitch, -mMaxPitch, mMaxPitch);
   // Make a quaternion representing pitch rotation,
   // which is about owner's right vector
   Quaternion q(mOwner->GetRight(), mPitch);
   
   // Rotate owner forward by pitch quaternion
   Vector3 viewForward = Vector3::Transform(
      mOwner->GetForward(), q);
   // Target position 100 units in front of view forward
   Vector3 target = cameraPos + viewForward * 100.0f;
   // Also rotate up by pitch quaternion
   Vector3 up = Vector3::Transform(Vector3::UnitZ, q);
   
   // Create look at matrix, set as view
   Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, up);
   SetViewMatrix(view);
}


Finally, FPSActor updates the pitch speed based on the relative y motion of the mouse. This requires code in ProcessInput that is almost identical to the code you use to update the angular speed based on the x motion from Listing 9.1. With this in place, the first-person camera now pitches without adjusting the pitch of the owning actor.

First-Person Model

Although it’s not strictly part of the camera, most first-person games also incorporate a first-person model. This model may have parts of an animated character, such as arms, feet, and so on. If the player carries a weapon, then when the player pitches up, the weapon appears to also aim up. You want the weapon model to pitch up even though the player character remains flat with the ground.

You can implement this with a separate actor for the first-person model. Then every frame, FPSActor updates the first-person model position and rotation. The position of the first-person model is the position of the FPSActor with an offset. This offset places the first-person model a little to the right of the actor. The rotation of the model starts with the rotation of the FPSActor but then has an additional rotation applied for the view pitch. Listing 9.4 shows the code for this.

Listing 9.4 Updating Position and Rotation of the First-Person Model


// Update position of FPS model relative to actor position
const Vector3 modelOffset(Vector3(10.0f, 10.0f, -10.0f));
Vector3 modelPos = GetPosition();
modelPos += GetForward() * modelOffset.x;
modelPos += GetRight() * modelOffset.y;
modelPos.z += modelOffset.z;
mFPSModel->SetPosition(modelPos);

// Initialize rotation to actor rotation
Quaternion q = GetRotation();

// Rotate by pitch from camera
q = Quaternion::Concatenate(q,
   Quaternion(GetRight(), mCameraComp->GetPitch()));
mFPSModel->SetRotation(q);


Figure 9.1 demonstrates the first-person camera with a first-person model. The aiming reticule is just a SpriteComponent positioned in the center of the screen.

Screenshot of the Game Programming in C++ (Chapter 5) window shows a circle at the top left representing a moon, a wall and a pointer at the center. A rifle is shown at the bottom right with a slider to the left.

Figure 9.1 First-person camera with first-person model

Follow Camera

A follow camera is a camera that follows behind a target object. This type of camera is popular in many games, including racing games where the camera follows behind a car and third-person action/adventure games such as Horizon Zero Dawn. Because follow cameras see use in many different types of games, there is a great deal of variety in their implementation. This section focuses on a follow camera tracking a car.

As was the case with the first-person character, you’ll create a new actor called FollowActor to correspond to the different style of movement when the game uses a follow camera. The movement controls are W/S to move the car forward and A/D to rotate the car left/right. The normal MoveComponent supports both types of movements, so it doesn’t require any changes here.

Basic Follow Camera

With a basic follow camera, the camera always follows a set distance behind and above the owning actor. Figure 9.2 gives the side view of this basic follow camera. The camera is a set horizontal distance HDist behind the car and a set vertical distance VDist above the car. The target point of the camera is not the car itself but a point TargetDist in front of the car. This causes the camera to look at a point a little in front of the car rather than directly at the car itself.

Figure represents basic follow camera tracking a car.

Figure 9.2 Basic follow camera tracking a car

To compute the camera position, you use vector addition and scalar multiplication. The camera position is HDist units behind the owner and VDist units above the owner, yielding the following equation:

Image

OwnerForward and OwnerUp in this equation are the owner’s forward and up vectors, respectively.

Similarly, TargetPos is just a point TargetDist units in front of the owner:

Image

In code, you declare a new subclass of CameraComponent called FollowCamera. It has member variables for the horizontal distance (mHorzDist), vertical distance (mVertDist), and target distance (mTargetDist). First, you create a function to compute the camera position (using the previous equation):

Vector3 FollowCamera::ComputeCameraPos() const
{
   // Set camera position behind and above owner
   Vector3 cameraPos = mOwner->GetPosition();
   cameraPos -= mOwner->GetForward() * mHorzDist;
   cameraPos += Vector3::UnitZ * mVertDist;
   return cameraPos;
}

Next, the FollowCamera::Update function uses this camera position as well as a computed target position to create the view matrix:

void FollowCamera::Update(float deltaTime)
{
   CameraComponent::Update(deltaTime);
   // Target is target dist in front of owning actor
   Vector3 target = mOwner->GetPosition() +
      mOwner->GetForward() * mTargetDist;
   // (Up is just UnitZ since we don't flip the camera)
   Matrix4 view = Matrix4::CreateLookAt(GetCameraPos(), target,
      Vector3::UnitZ);
   SetViewMatrix(view);
}

Although this basic follow camera successfully tracks the car as it moves through the game world, it appears very rigid. Because the camera is always a set distance from the target, it’s difficult to get a sense of speed. Furthermore, when the car turns, it almost seems like the world—not the car—is turning. So even though the basic follow camera is a good starting point, it’s not a very polished solution.

One simple change that improves the sense of speed is to make the horizontal follow distance a function of the speed of the owner. Perhaps at rest the horizontal distance is 350 units, but when moving at max speed it increases to 500. This makes it easier to perceive the speed of the car, but the camera still seems stiff when the car is turning. To solve the rigidity of the basic follow camera, you can add springiness to the camera.

Adding a Spring

Rather than having the camera position instantly changing to the position as per the equation, you can have the camera adjust to this position over the course of several frames. To accomplish this, you can separate the camera position into an “ideal” camera position and an “actual” camera position. The ideal camera position is the position derived from the basic follow camera equations, while the actual camera position is what the view  matrix uses.

Now, imagine that there’s a spring connecting the ideal camera and the actual camera. Initially, both cameras are at the same location. As the ideal camera moves, the spring stretches and the actual camera also starts to move—but at a slower rate. Eventually, the spring stretches completely, and the actual camera moves just as quickly as the ideal camera. Then, when the ideal camera stops, the spring eventually compresses back to its steady state. At this point, the ideal camera and actual camera are at the same point again. Figure 9.3 visualizes this idea of a spring connecting the ideal and actual cameras.

Figure represents a spring connecting the ideal and actual cameras.

Figure 9.3 A spring connecting the ideal and actual cameras

Implementing a spring requires a few more member variables in FollowCamera. A spring constant (mSpringConstant) represents the stiffness of the spring, with a higher value being stiffer. You also must track the actual position (mActualPos) and the velocity (mVelocity) of the camera from frame to frame, so you add two vector member variables for these.

Listing 9.5 gives the code for FollowCamera::Update with a spring. First, you compute a spring dampening based on the spring constant. Next, the ideal position is simply the position from the previously implemented ComputeCameraPos function. You then compute the difference between the actual and ideal positions and compute an acceleration of the camera based on this distance and a dampening of the old velocity. Next, you compute the velocity and acceleration of the camera by using the Euler integration technique introduced in Chapter 3, “Vectors and Basic Physics.” Finally, the target position calculation remains the same, and the CreateLookAt function now uses the actual position as opposed to the ideal one.

Listing 9.5 FollowCamera::Update Implementation (with Spring)


void FollowCamera::Update(float deltaTime)
{
   CameraComponent::Update(deltaTime);
   
   // Compute dampening from spring constant
   float dampening = 2.0f * Math::Sqrt(mSpringConstant);
   
   // Compute ideal position
   Vector3 idealPos = ComputeCameraPos();
   
   // Compute difference between actual and ideal
   Vector3 diff = mActualPos - idealPos;
   // Compute acceleration of spring
   Vector3 acel = -mSpringConstant * diff -
      dampening * mVelocity;
   
   // Update velocity
   mVelocity += acel * deltaTime;
   // Update actual camera position
   mActualPos += mVelocity * deltaTime;
   
   // Target is target dist in front of owning actor
   Vector3 target = mOwner->GetPosition() +
      mOwner->GetForward() * mTargetDist;
   
   // Use actual position here, not ideal
   Matrix4 view = Matrix4::CreateLookAt(mActualPos, target,
      Vector3::UnitZ);
   SetViewMatrix(view);
}


A big advantage of using a spring camera is that when the owning object turns, the camera takes a moment to catch up to the turn. This means that the side of the owning object is visible as it turns. This gives a much better sense that the object, not the world, is turning. Figure 9.4 shows the spring follow camera in action.

Screenshot shows spring follow camera following a car as it turns.

Figure 9.4 Spring follow camera following a car as it turns

The red sports car model used here is “Racing Car” by Willy Decarpentrie, licensed under CC Attribution and downloaded from https://sketchfab.com.

Finally, to make sure the camera starts out correctly at the beginning of the game, you create a SnapToIdeal function that’s called when the FollowActor first initializes:

void FollowCamera::SnapToIdeal()
{
   // Set actual position to ideal
   mActualPos = ComputeCameraPos();
   // Zero velocity
   mVelocity = Vector3::Zero;
   // Compute target and view
   Vector3 target = mOwner->GetPosition() +
      mOwner->GetForward() * mTargetDist;
   Matrix4 view = Matrix4::CreateLookAt(mActualPos, target,
      Vector3::UnitZ);
   SetViewMatrix(view);
}

Orbit Camera

An orbit camera focuses on a target object and orbits around it. This type of camera might be used in a builder game such as Planet Coaster, as it allows the player to easily see the area around an object. The simplest implementation of an orbit camera stores the camera’s position as an offset from the target rather than as an absolute world space position. This takes advantage of the fact that rotations always rotate about the origin. So, if the camera position is an offset from the target object, any rotations are effectively about the target object.

In this section, you’ll create an OrbitActor as well as an OrbitCamera class. A typical control scheme uses the mouse for both yaw and pitch around the object. The input code that converts relative mouse movement into rotation values is like the code covered in the “First-Person Camera” section, earlier in this chapter. However, you add a restriction that the camera rotates only when the player is holding down the right mouse button (since this is a typical control scheme). Recall that the SDL_GetRelativeMouseState function returns the state of the buttons. The following conditional tests whether the player is holding the right mouse button:

if (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT))

The OrbitCamera class requires the following member variables:

// Offset from target
Vector3 mOffset;
// Up vector of camera
Vector3 mUp;
// Rotation/sec speed of pitch
float mPitchSpeed;
// Rotation/sec speed of yaw
float mYawSpeed;

The pitch speed (mPitchSpeed) and yaw speed (mYawSpeed) simply track the current rotations per second of the camera for each type of rotation. The owning actor can update these speeds as needed, based on the mouse rotation. In addition, the OrbitCamera needs to track the offset of the camera (mOffset), as well as the up vector of the camera (mUp). The up vector is needed because the orbit camera allows full 360-degree rotations in both yaw and pitch. This means the camera could flip upside down, so you can’t universally pass in (0, 0, 1) as up. Instead, you must update the up vector as the camera rotates.

The constructor for OrbitCamera initializes mPitchSpeed and mYawSpeed both to zero. The mOffset vector can initialize to any value, but here you initialize it to 400 units behind the object (-400, 0, 0). The mUp vector initializes to world up (0, 0, 1).

Listing 9.6 shows the implementation of OrbitCamera::Update. First, you create a quaternion representing the amount of yaw to apply this frame, which is about the world up vector. You use this quaternion to transform both the camera offset and up. Next, you compute the camera forward vector from the new offset. The cross product between the camera forward and camera yields the camera right vector. You then use this camera right vector to compute the pitch quaternion and transform both the camera offset and up by this quaternion, as well.

Listing 9.6 OrbitCamera::Update Implementation


void OrbitCamera::Update(float deltaTime)
{
   CameraComponent::Update(deltaTime);
   // Create a quaternion for yaw about world up
   Quaternion yaw(Vector3::UnitZ, mYawSpeed * deltaTime);
   // Transform offset and up by yaw
   mOffset = Vector3::Transform(mOffset, yaw);
   mUp = Vector3::Transform(mUp, yaw);

   // Compute camera forward/right from these vectors
   // Forward owner.position - (owner.position + offset)
   // = -offset
   Vector3 forward = -1.0f * mOffset;
   forward.Normalize();
   Vector3 right = Vector3::Cross(mUp, forward);
   right.Normalize();
   
   // Create quaternion for pitch about camera right
   Quaternion pitch(right, mPitchSpeed * deltaTime);
   // Transform camera offset and up by pitch
   mOffset = Vector3::Transform(mOffset, pitch);
   mUp = Vector3::Transform(mUp, pitch);

   // Compute transform matrix
   Vector3 target = mOwner->GetPosition();
   Vector3 cameraPos = target + mOffset;
   Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, mUp);
   SetViewMatrix(view);
}


For the look-at matrix, the target position of the camera is simply the owner’s position,  the camera position is the owner’s position plus the offset, and the up is the camera up. This yields the final orbited camera. Figure 9.5 demonstrates the orbit camera with the car  as the target.

Screenshot shows orbit camera focused on the car.

Figure 9.5 Orbit camera focused on the car

Spline Camera

A spline is a mathematical representation of a curve specified by a series of points on the curve. Splines are popular in games because they enable an object to smoothly move along a curve over some period. This can be very useful for a cutscene camera because the camera can follow a predefined spline path. This type of camera also sees use in games like God of War, where the camera follows along a set path as the player progresses through the world.

The Catmull-Rom spline is a type of spline that’s relatively simple to compute, and it is therefore used frequently in games and computer graphics. This type of spline minimally requires four control points, named P0 through P3. The actual curve runs from P1 to P2, while P0 is a control point prior to the curve and P3 is a control point after the curve. For best results, you can space these control points roughly evenly along the curve—and you can approximate this with Euclidean distance. Figure 9.6 illustrates a Catmull-Rom spline with four control points.

Figure represents a Catmull-Rom spline.

Figure 9.6 Catmull-Rom spline

Given these four control points, you can express the position between P1 and P2 as the following parametric equation, where t = 0 is at P1 and t = 1 is at P2:

Image

Although the Catmull-Rom spline equation has only four control points, you can extend the spline to any arbitrary number of control points. This works provided that there still is one point before the path and one point after the path because those control points are not part of the path. In other words, you need n + 2 points to represent a curve of n points. You can then take any sequence of four neighboring points and substitute them into the spline equation.

To implement a camera that follows a spline path, you first create a struct to define a spline. The only member data Spline needs is a vector of the control points:

struct Spline
{
   // Control points for spline
   // (Requires n + 2 points where n is number
   // of points in segment)
   std::vector<Vector3> mControlPoints;
   // Given spline segment where startIdx = P1,
   // compute position based on t value
   Vector3 Compute(size_t startIdx, float t) const;
   size_t GetNumPoints() const { return mControlPoints.size(); }
};

The Spline::Compute function applies the spline equation given a start index corresponding to P1 and a t value in the range [0.0, 1.0]. It also performs boundary checks to make sure startIdx is a valid index, as shown in Listing 9.7.

Listing 9.7 Spline::Compute Implementation


Vector3 Spline::Compute(size_t startIdx, float t) const
{
   // Check if startIdx is out of bounds
   if (startIdx >= mControlPoints.size())
   { return mControlPoints.back(); }
   else if (startIdx == 0)
   { return mControlPoints[startIdx]; }
   else if (startIdx + 2 >= mControlPoints.size())
   { return mControlPoints[startIdx]; }

   // Get p0 through p3
   Vector3 p0 = mControlPoints[startIdx - 1];
   Vector3 p1 = mControlPoints[startIdx];
   Vector3 p2 = mControlPoints[startIdx + 1];
   Vector3 p3 = mControlPoints[startIdx + 2];

   // Compute position according to Catmull-Rom equation
   Vector3 position = 0.5f * ((2.0f * p1) + (-1.0f * p0 + p2) * t +
      (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t * t +
      (-1.0f * p0 + 3.0f * p1 - 3.0f * p2 + p3) * t * t * t);
   return position;
}


The SplineCamera class then needs a Spline in its member data. In addition, it tracks the current index corresponding to P1, the current t value, a speed, and whether the camera should move along the path:

// Spline path camera follows
Spline mPath;
// Current control point index and t
size_t mIndex;
float mT;
// Amount t changes/sec
float mSpeed;
// Whether to move the camera along the path
bool mPaused;

The spline camera updates by first increasing the t value as a function of speed and delta time. If the t value is greater than or equal to 1.0, P1 advances to the next point on the path (assuming that there are enough points on the path). Advancing P1 also means you must subtract 1 from the t value. If the spline has no more points, the spline camera pauses.

For the camera calculations, the position of the camera is simply the point computed from the spline. To compute the target point, you increase t by a small delta to determine the direction the spline camera is moving. Finally, the up vector stays at (0, 0, 1), which assumes that you do not want the spline to flip upside down. Listing 9.8 gives the code for SplineCamera::Update, and Figure 9.7 shows the spline camera in action.

Screenshot of the Game Programming in C++ (Chapter 9) window shows a circle at the top left and a slider at the bottom left. A view of the walls is shown along with the flooring.

Figure 9.7 Spline camera in a game

Listing 9.8 SplineCamera::Update Implementation


void SplineCamera::Update(float deltaTime)
{
   CameraComponent::Update(deltaTime);
   // Update t value
   if (!mPaused)
   {
      mT += mSpeed * deltaTime;
      // Advance to the next control point if needed.
      // This assumes speed isn't so fast that you jump past
      // multiple control points in one frame.
      if (mT >= 1.0f)
      {
         // Make sure we have enough points to advance the path
         if (mIndex < mPath.GetNumPoints() - 3)
         {
            mIndex++;
            mT = mT - 1.0f;
         }
         else
         {
            // Path's done, so pause
            mPaused = true;
         }
      }
   }
   
   // Camera position is the spline at the current t/index
   Vector3 cameraPos = mPath.Compute(mIndex, mT);
   // Target point is just a small delta ahead on the spline
   Vector3 target = mPath.Compute(mIndex, mT + 0.01f);
   
// Assume spline doesn't flip upside-down
   const Vector3 up = Vector3::UnitZ;
   Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, up);
   SetViewMatrix(view);
}


Unprojection

Given a point in world space, to transform it into clip space, you first multiply by the view matrix followed by the projection matrix. Imagine that the player in a first-person shooter wants to fire a projectile based on the screen position of the aiming reticule. In this case, the aiming reticule position is a coordinate in screen space, but to correctly fire the projectile, you need a position in world space. An unprojection is a calculation that takes in a screen space coordinate and converts it into a world space coordinate.

Assuming the screen space coordinate system described in Chapter 5, “OpenGL,” the center of the screen is (0, 0), the top-left corner is (-512, 384), and the bottom-right corner is (512, -384). The first step to calculating an unprojection is converting a screen space coordinate into a normalized device coordinate with a range of [-1, 1] for both the x and y components:

Image

However, the issue is that any single (x, y) coordinate can correspond to any z coordinate in the range [0, 1], where 0 is a point on the near plane (right in front of the camera), and 1 is a point on the far plane (the maximum distance you can see from the camera). So, to correctly perform the unprojection, you also need a z component in the range [0, 1]. You then represent this as a homogenous coordinate:

Image

Now you construct an unprojection matrix, which is simply the inverse of the view-projection matrix:

Image

When multiplying the NDC point by the unprojection matrix, the w component changes. However, you need to renormalize the w component (setting it back to 1) by dividing each component by w. This yields the following calculation for the point in world space:

Image

You add a function for an unprojection into the Renderer class because it’s the only class with access to both the view and projection matrices. Listing 9.9 provides the implementation for Unproject. In this code, the TransformWithPerspDiv function does the w component renormalization.

Listing 9.9 Renderer::Unproject Implementation


Vector3 Renderer::Unproject(const Vector3& screenPoint) const
{
   // Convert screenPoint to device coordinates (between -1 and +1)
   Vector3 deviceCoord = screenPoint;
   deviceCoord.x /= (mScreenWidth) * 0.5f;
   deviceCoord.y /= (mScreenHeight) * 0.5f;

   // Transform vector by unprojection matrix
   Matrix4 unprojection = mView * mProjection;
   unprojection.Invert();
   return Vector3::TransformWithPerspDiv(deviceCoord, unprojection);
}


You can use Unproject to calculate a single world space position. However, it some cases, it’s more useful to construct a vector in the direction of the screen space point, as it gives opportunities for other useful features. One such feature is picking, which is the capability to click to select an object in the 3D world. Figure 9.8 illustrates picking with a mouse cursor.

Figure represents the process of picking with a vector in the direction of the screen space coordinate of the mouse.

Figure 9.8 Picking with a vector in the direction of the screen space coordinate of the mouse

To construct a direction vector, you use Unproject twice, once for a start point and once for the end point. Then simply use vector subtraction and normalize this vector, as in the implementation of Renderer::GetScreenDirection in Listing 9.10. Note how the function computes both the start point of the vector in world space and the direction.

Listing 9.10 Renderer::GetScreenDirection Implementation


void Renderer::GetScreenDirection(Vector3& outStart,
   Vector3& outDir) const
{
   // Get start point (in center of screen on near plane)
   Vector3 screenPoint(0.0f, 0.0f, 0.0f);
   outStart = Unproject(screenPoint);

   // Get end point (in center of screen, between near and far)
   screenPoint.z = 0.9f;
   Vector3 end = Unproject(screenPoint);

   // Get direction vector
   outDir = end - outStart;
   outDir.Normalize();
}


Game Project

This chapter’s game project demonstrates all the different cameras discussed in the chapter, as well as the unprojection code. The code is available in the book’s GitHub repository, in the Chapter09 directory. Open Chapter09-windows.sln on Windows and Chapter09-mac.xcodeproj on Mac.

The camera starts out in first-person mode. To switch between the different cameras, use the 1 through 4 keys:

  • 1—Enable first-person camera mode

  • 2—Enable follow camera mode

  • 3—Enable orbit camera mode

  • 4—Enable spline camera mode and restart the spline path

Depending on the camera mode, the character has different controls, summarized below:

  • First-person—Use W/S to move forward and back, A/D to strafe, and the mouse to rotate

  • Follow—Use W/S to move forward and back and use A/D to rotate (yaw)

  • Orbit camera mode—Hold down the right mouse button and move the mouse to rotate

  • Spline camera mode—No controls (moves automatically)

In addition, in any camera mode, you can left-click to compute the unprojection. This positions two spheres—one at the “start” position of the vector and one at the “end” position.

Summary

This chapter shows how to implement many different types of cameras. The first-person camera presents the world from the perspective of a character moving through it. A typical first-person control scheme uses the WASD keys for movement and the mouse for rotation. Moving the mouse left and right rotates the character, while moving the mouse up and down pitches the view. You can additionally use the first-person view pitch to orient a first-person model.

A basic follow camera follows rigidly behind an object. However, this camera does not look polished when rotating because it’s difficult to discern if the character or the world is rotating. An improvement is to incorporate a spring between “ideal” and “actual” camera positions. This adds smoothness to the camera that’s especially noticeable when turning.

An orbit camera rotates around an object, typically with mouse or joystick control. To implement orbiting, you represent the camera as an offset from the target object. Then, you can apply both yaw and pitch rotations by using quaternions and some vector math to yield the final view.

A spline is a curve defined by points on the curve. Splines are popular for cutscene cameras. The Catmull-Rom spline requires a minimum of n + 2 points to represent a curve of n points. By applying the Catmull-Rom spline equations, you can create a camera that follows along this spline path.

Finally, an unprojection has many uses, such as selecting or picking objects with the mouse. To compute an unprojection, you first transform a screen space point into normalized device coordinates. You then multiply by the unprojection matrix, which is simply the inverse of the view-projection matrix.

Additional Reading

There are not many books dedicated to the topic of game cameras. However, Mark Haigh-Hutchinson, the primary programmer for the Metroid Prime camera system, provides an overview of many different techniques relevant for game cameras.

Haigh-Hutchinson, Mark. Real-Time Cameras. Burlington: Morgan Kaufmann, 2009.

Exercises

In this chapter’s exercises, you will add features to some of the cameras. In the first exercise, you add mouse support to the follow camera, and in the second exercise, you add features to the spline camera.

Exercise 9.1

Many follow cameras have support for user-controlled rotation of the camera. For this exercise, add code to the follow camera implementation that allows the user to rotate the camera. When the player holds down the right mouse button, apply an additional pitch and yaw rotation to the camera. When the player releases the right mouse button, set the pitch/yaw rotation back to zero.

The code for the rotation is like the rotation code for the orbit camera. Furthermore, as with the orbit camera, the code can no longer assume that the z-axis is up. When the player releases the mouse button, the camera won’t immediately snap back to the original orientation because of the spring. However, this is aesthetically pleasing, so there’s no reason to change this behavior!

Exercise 9.2

Currently, the spline camera goes in only one direction on the path and stops upon reaching the end. Modify the code so that when the spline hits the end of the path, it starts moving backward.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.150.123