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.
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.
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
.
// 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);
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.
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);
}
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.
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.
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.
// 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.
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.
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.
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:
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:
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.
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.
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.
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.
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);
}
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.
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.
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.
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:
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.
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.
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);
}
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:
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:
Now you construct an unprojection matrix, which is simply the inverse of the view-projection matrix:
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:
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.
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.
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.
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();
}
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.
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.
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.
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.
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!
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.
18.217.150.123