Chapter 13. Cameras

In this chapter, you create a reusable camera system to visualize your 3D scenes. You first develop a base camera class to support common functionality, and then you extend the class to create a first-person camera controlled by the mouse and keyboard.

A Base Camera Component

The topic of a virtual camera is peppered throughout this text, and you can’t visualize your 3D scenes without one. But there’s no one-size-fits-all camera to meet the needs of all your applications. For example, you might want an orbit camera, similar to what you used in NVIDIA FX Composer. Or perhaps you’re building an application that needs a first-person camera, in which you use the mouse to control the pitch and yaw, and you use the W, A, S, and D keys to control movement. Maybe you’re building a 2.5D platformer (a side-scroller that’s rendered in 3D but viewed with a fixed-axis camera) or an action game with an over-the-shoulder third-person camera that “chases” the avatar with a motion that suggests it’s attached with springs. You can implement many types of cameras, but all of them share a base set of functionality that’s necessary without respect to the specific behavior of the camera. In this section, you develop a general-purpose camera component that’s intended for use as a base class. Then you extend this class to create a first-person camera you can use for future demos.

Camera Theory Revisited

Recall the discussion of cameras from Chapter 2, “A 3D/Math Primer.” There you learned that the properties of a camera create a view frustum—sort of a pyramid with the top chopped off—and that only objects within the frustum are visible. A view frustum is created through the camera’s position (in world space), a vector describing where the camera is looking, and a vector describing which way is up. Additional properties include the camera’s vertical field of view, the aspect ratio (width over height), and the distances of the near and far planes. You can consider these properties as the inputs to your camera. Also recall that these properties define the transformation matrices for moving objects into view space and projection space. Thus, the outputs of a camera can be considered the view and projection matrices, or a combined view-projection matrix.

Listing 13.1 declares the Camera class, which includes members for these inputs and outputs. This class has a full implementation and can be used as is within your applications. Indeed, you’ll use this camera in the next chapter to render your first 3D scene. However, it’s really intended as a base class and has no functionality for the specific motion of the camera. That type of behavior is delegated to the derived classes.

Listing 13.1 The Camera.h Header File


#pragma once

#include "GameComponent.h"

namespace Library
{
    class GameTime;

    class Camera : public GameComponent
    {
        RTTI_DECLARATIONS(Camera, GameComponent)

    public:
        Camera(Game& game);
        Camera(Game& game, float fieldOfView, float aspectRatio, float
nearPlaneDistance, float farPlaneDistance);

        virtual ~Camera();

        const XMFLOAT3& Position() const;
        const XMFLOAT3& Direction() const;
        const XMFLOAT3& Up() const;
        const XMFLOAT3& Right() const;

        XMVECTOR PositionVector() const;
        XMVECTOR DirectionVector() const;
        XMVECTOR UpVector() const;
        XMVECTOR RightVector() const;

        float AspectRatio() const;
        float FieldOfView() const;
        float NearPlaneDistance() const;
        float FarPlaneDistance() const;

        XMMATRIX ViewMatrix() const;
        XMMATRIX ProjectionMatrix() const;
        XMMATRIX ViewProjectionMatrix() const;

        virtual void SetPosition(FLOAT x, FLOAT y, FLOAT z);
        virtual void SetPosition(FXMVECTOR position);
        virtual void SetPosition(const XMFLOAT3& position);

        virtual void Reset();
        virtual void Initialize() override;
        virtual void Update(const GameTime& gameTime) override;
        virtual void UpdateViewMatrix();
        virtual void UpdateProjectionMatrix();
        void ApplyRotation(CXMMATRIX transform);
        void ApplyRotation(const XMFLOAT4X4& transform);

        static const float DefaultFieldOfView;
        static const float DefaultAspectRatio;
        static const float DefaultNearPlaneDistance;
        static const float DefaultFarPlaneDistance;

    protected:
        float mFieldOfView;
        float mAspectRatio;
        float mNearPlaneDistance;
        float mFarPlaneDistance;

        XMFLOAT3 mPosition;
        XMFLOAT3 mDirection;
        XMFLOAT3 mUp;
        XMFLOAT3 mRight;

        XMFLOAT4X4 mViewMatrix;
        XMFLOAT4X4 mProjectionMatrix;

    private:
        Camera(const Camera& rhs);
        Camera& operator=(const Camera& rhs);
    };
}


Before diving into the implementation, let’s examine the structure of the Camera class. As you can see, a Camera is a game component and can therefore be initialized and updated by the base Game class without explicit calls to the associated methods. Instead, you simply add a Camera instance to the Game::mComponents member. This class is also a good candidate for inclusion in the service container because it is useful to any component that needs to draw 3D objects to the screen.

The Camera class has members for the field of view, aspect ratio, and near and far plane distances, and a constructor accepts arguments for these values. A default constructor also uses the quantities stored in the static members: DefaultFieldOfView, DefaultAspectRatio, DefaultNearPlaneDistance, and DefaultFarPlaneDistance.

Three members define the orientation of the camera in three-dimensional space: mDirection, mUp, and mRight. These vectors are orthogonal to each other and are rotated in concert. (More specifically, the direction and up vectors are rotated, and the right vector is derived through a cross product of the other two vectors.) The mPosition member stores the location of the camera.

The two outputs of the camera are the mViewMatrix and mProjectionMatrix members. We describe the Direct3D methods, used to update these values, after the code presentation. Listing 13.2 presents an abbreviated implementation of Camera class. For brevity, the listing omits most of the single-line accessors and mutators. You can find the full source code on the book’s companion website.

Listing 13.2 The Camera Class Implementation (Abbreviated)


#include "Camera.h"
#include "Game.h"
#include "GameTime.h"
#include "VectorHelper.h"
#include "MatrixHelper.h"

namespace Library
{
    RTTI_DEFINITIONS(Camera)

    const float Camera::DefaultFieldOfView = XM_PIDIV4;
    const float Camera::DefaultNearPlaneDistance = 0.01f;
    const float Camera::DefaultFarPlaneDistance = 1000.0f;

    Camera::Camera(Game& game)
        : GameComponent(game),
          mFieldOfView(DefaultFieldOfView),
          mAspectRatio(game.AspectRatio()),
          mNearPlaneDistance(DefaultNearPlaneDistance),
          mFarPlaneDistance(DefaultFarPlaneDistance),
          mPosition(), mDirection(), mUp(), mRight(),
          mViewMatrix(), mProjectionMatrix()
    {
    }

    XMMATRIX Camera::ViewProjectionMatrix() const
    {
        XMMATRIX viewMatrix = XMLoadFloat4x4(&mViewMatrix);
        XMMATRIX projectionMatrix = XMLoadFloat4x4(&mProjectionMatrix);

        return XMMatrixMultiply(viewMatrix, projectionMatrix);
    }

    void Camera::SetPosition(FLOAT x, FLOAT y, FLOAT z)
    {
        XMVECTOR position = XMVectorSet(x, y, z, 1.0f);
        SetPosition(position);
    }

    void Camera::SetPosition(FXMVECTOR position)
    {
        XMStoreFloat3(&mPosition, position);
    }

    void Camera::SetPosition(const XMFLOAT3& position)
    {
        mPosition = position;
    }

    void Camera::Reset()
    {
        mPosition = Vector3Helper::Zero;
        mDirection = Vector3Helper::Forward;
        mUp = Vector3Helper::Up;
        mRight = Vector3Helper::Right;

        UpdateViewMatrix();
    }

    void Camera::Initialize()
    {
        UpdateProjectionMatrix();
        Reset();
    }

    void Camera::Update(const GameTime& gameTime)
    {
        UpdateViewMatrix();
    }

    void Camera::UpdateViewMatrix()
    {
        XMVECTOR eyePosition = XMLoadFloat3(&mPosition);
        XMVECTOR direction = XMLoadFloat3(&mDirection);
        XMVECTOR upDirection = XMLoadFloat3(&mUp);

        XMMATRIX viewMatrix = XMMatrixLookToRH(eyePosition, direction,
upDirection);
        XMStoreFloat4x4(&mViewMatrix, viewMatrix);
    }

    void Camera::UpdateProjectionMatrix()
    {
        XMMATRIX projectionMatrix = XMMatrixPerspectiveFovRH
(mFieldOfView, mAspectRatio, mNearPlaneDistance, mFarPlaneDistance);
        XMStoreFloat4x4(&mProjectionMatrix, projectionMatrix);
    }

    void Camera::ApplyRotation(CXMMATRIX transform)
    {
        XMVECTOR direction = XMLoadFloat3(&mDirection);
        XMVECTOR up = XMLoadFloat3(&mUp);

        direction = XMVector3TransformNormal(direction, transform);
        direction = XMVector3Normalize(direction);

        up = XMVector3TransformNormal(up, transform);
        up = XMVector3Normalize(up);

        XMVECTOR right = XMVector3Cross(direction, up);
        up = XMVector3Cross(right, direction);

        XMStoreFloat3(&mDirection, direction);
        XMStoreFloat3(&mUp, up);
        XMStoreFloat3(&mRight, right);
    }

    void Camera::ApplyRotation(const XMFLOAT4X4& transform)
    {
        XMMATRIX transformMatrix = XMLoadFloat4x4(&transform);
        ApplyRotation(transformMatrix);
    }
}


DirectXMath Usage

The first area to examine is the DirectXMath usage found throughout the Camera class implementation. For example, the Camera::ViewProjectionMatrix() accessor first loads the mViewMatrix and mProjectionMatrix members into XMMATRIX variables and then multiplies them with the XMMatrixMultiply() function. Recall the discussion of DirectXMath from Chapter 2 and the performance benefits of SIMD instructions. Matrix and vector operations should use SIMD variables, but SIMD types are 16-byte aligned and are therefore not good candidates for class member storage. Thus, you store the view and projection matrices as XMFLOAT4X4 class members and load them into XMMATRIX variables to use them. Likewise, you store three-component vectors with XMFLOAT3 class members and load them into XMVECTOR objects for calculations.

Conversely, if you need to save computed SIMD variables back to associated class members, you do so with store calls, such as XMStoreFloat3() and XMStoreFloat4×4(). You can see this usage in the Camera::UpdateViewMatrix() and Camera::ApplyRotation() methods.


Note

An alternative to using DirectX Math load and store methods is to align your classes and structs along 16-byte boundaries with __declspec(align(16)). You can find more information on data alignment at http://msdn.microsoft.com/en-us/library/83ythb65.aspx.


The Reset() Method

The Camera::Reset() method sets the position and orientation members to reasonable values and then invokes the Camera::UpdateViewMatrix() methods. Updating the mViewMatrix member is necessary whenever the position or orientation of the camera changes. As with most of the Camera methods, the Reset() method is virtual and can be customized by a derived class.

The Reset() method uses a Vector3Helper class that has a number of useful static XMFLOAT3 members. Analogous Vector2Helper and Vector4Helper classes exist for 2D and 4D vectors. You can find these helper classes in the VectorHelper.h header file on the book’s companion website.

The UpdateViewMatrix() Method

The Camera::UpdateViewMatrix() method uses the XMMatrixLookToRH() method from the DirectXMath library. This method creates a view matrix for a right-handed coordinate system. An XMMatrixLookToLH() method creates a view matrix for a left-handed coordinate system. Both methods accept three XMVECTOR arguments for the camera’s position, direction, and up vector. These values are loaded from the associated class members.

Note that the Camera::Update() method invokes the UpdateViewMatrix() method each frame. You could consider an optimization that checked a “dirty” status to opt out of the view matrix calculation if the camera hasn’t moved.

The UpdateProjectionMatrix() Method

Along with helper methods for computing the view matrix, the DirectXMath library includes methods for computing the projection matrix. For perspective projection, the camera employs XMMatrixPerspectiveFovRH(), which accepts arguments for the field of view, aspect ratio, and near and far plane distances. This method also has a left-handed version.

Whereas the view matrix is expected to update regularly, the projection matrix is not. Indeed, it’s not uncommon to set the projection matrix once (at camera initialization) and not change it again throughout the program’s execution. That’s the approach taken here. Note that the associated class members are protected and have no public mutators. However, a derived class could expose these members or otherwise allow modification of the projection matrix post-initialization.

The ApplyRotation() Method

Although the base Camera class doesn’t provide implicit behavior for the camera’s movement, it does include an ApplyRotation() method. This method applies a rotation matrix to the orientation of the camera. This is accomplished by transforming the direction and up vectors by the rotation matrix and then computing the right vector as the cross product of the other two vectors. A second cross product exists, which might seem rather strange. Immediately after the right vector is computed, the up vector is recalculated as a cross product of the right and direction vectors. This step is intended to eliminate any computation error and guarantee that the three vectors are orthogonal.

Note that any translation passed into the ApplyRotation() method does not modify the camera’s position. Instead, you can change the camera’s position through the overloaded SetPosition() methods.

A First-Person Camera

Now let’s extend the base Camera class to implement a first-person camera. This camera is controlled through a combination of the mouse and keyboard. The W and S keys move the camera forward and backward along its direction vector. The A and D keys “strafe” (move the camera horizontally) along the right vector. The mouse controls the yaw and pitch of the camera: You move the mouse vertically to pitch, and horizontally to yaw. Note that pitch rotation is performed around the camera’s right vector, and yaw is around the y-axis. This is the traditional behavior of a first-person camera, but it can’t accommodate, for example, a game set in space in which the camera would require roll (longitudinal rotation) as well.

Listing 13.3 presents the header file for the FirstPersonCamera class.

Listing 13.3 The FirstPersonCamera.h Header File


#pragma once

#include "Camera.h"

namespace Library
{
    class Keyboard;
    class Mouse;

    class FirstPersonCamera : public Camera
    {
        RTTI_DECLARATIONS(FirstPersonCamera, Camera)

    public:
        FirstPersonCamera(Game& game);
        FirstPersonCamera(Game& game, float fieldOfView, float
aspectRatio, float nearPlaneDistance, float farPlaneDistance);

        virtual ~FirstPersonCamera();

        const Keyboard& GetKeyboard() const;
        void SetKeyboard(Keyboard& keyboard);

        const Mouse& GetMouse() const;
        void SetMouse(Mouse& mouse);

        float& MouseSensitivity();
        float& RotationRate();
        float& MovementRate();

        virtual void Initialize() override;
        virtual void Update(const GameTime& gameTime) override;

        static const float DefaultMouseSensitivity;
        static const float DefaultRotationRate;
        static const float DefaultMovementRate;

    protected:
        float mMouseSensitivity;
        float mRotationRate;
        float mMovementRate;

        Keyboard* mKeyboard;
        Mouse* mMouse;

    private:
        FirstPersonCamera(const FirstPersonCamera& rhs);
        FirstPersonCamera& operator=(const FirstPersonCamera& rhs);
    };
}


The FirstPersonCamera class has members for the mouse and keyboard, and it exposes them through public accessors and mutators. However, as you see in the implementation, the Initialize() method attempts to find the mouse and keyboard within the service container. The mutators simply enable you to override these settings and even disable mouse or keyboard control by nullifying these values.

The class also contains members for movement and rotation rates and mouse sensitivity. The “rate” variables specify how many units the camera should translate or rotate in 1 second. The mouse sensitivity enables you to amplify or dampen the input of the mouse.

Listing 13.4 presents the more interesting aspects of the camera implementation. Complete source code is available on the companion website.

Listing 13.4 The FirstPersonCamera Class Implementation (Abbreviated)


const float FirstPersonCamera::DefaultRotationRate =
XMConvertToRadians(1.0f);
const float FirstPersonCamera::DefaultMovementRate = 10.0f;
const float FirstPersonCamera::DefaultMouseSensitivity = 100.0f;

void FirstPersonCamera::Initialize()
{
    mKeyboard = (Keyboard*)mGame->Services().GetService(Keyboard::Type
IdClass());
    mMouse = (Mouse*)mGame->Services().GetService
(Mouse::TypeIdClass());

    Camera::Initialize();
}

void FirstPersonCamera::Update(const GameTime& gameTime)
{
    XMFLOAT2 movementAmount = Vector2Helper::Zero;
    if (mKeyboard != nullptr)
    {
        if (mKeyboard->IsKeyDown(DIK_W))
        {
            movementAmount.y = 1.0f;
        }

        if (mKeyboard->IsKeyDown(DIK_S))
        {
            movementAmount.y = -1.0f;
        }

        if (mKeyboard->IsKeyDown(DIK_A))
        {
            movementAmount.x = -1.0f;
        }

        if (mKeyboard->IsKeyDown(DIK_D))
        {
            movementAmount.x = 1.0f;
        }
    }

    XMFLOAT2 rotationAmount = Vector2Helper::Zero;
    if ((mMouse != nullptr) && (mMouse->IsButtonHeldDown
(MouseButtonsLeft)))
    {
        LPDIMOUSESTATE mouseState = mMouse->CurrentState();
        rotationAmount.x = -mouseState->lX * mMouseSensitivity;
        rotationAmount.y = -mouseState->lY * mMouseSensitivity;
    }

    float elapsedTime = (float)gameTime.ElapsedGameTime();
    XMVECTOR rotationVector = XMLoadFloat2(&rotationAmount) *
mRotationRate * elapsedTime;
    XMVECTOR right = XMLoadFloat3(&mRight);

    XMMATRIX pitchMatrix = XMMatrixRotationAxis(right,
XMVectorGetY(rotationVector));
    XMMATRIX yawMatrix = XMMatrixRotationY(XMVectorGetX
(rotationVector));

    ApplyRotation(XMMatrixMultiply(pitchMatrix, yawMatrix));

    XMVECTOR position = XMLoadFloat3(&mPosition);
    XMVECTOR movement = XMLoadFloat2(&movementAmount) * mMovementRate *
elapsedTime;

    XMVECTOR strafe = right * XMVectorGetX(movement);
    position += strafe;

    XMVECTOR forward = XMLoadFloat3(&mDirection) *
XMVectorGetY(movement);
    position += forward;

    XMStoreFloat3(&mPosition, position);

    Camera::Update(gameTime);
}


The default values for the rotation rate, movement rate, and mouse sensitivity are set at the beginning of Listing 13.4. Note the XMConvertToRadians() function: This is a DirectXMath helper function that converts degrees to radians. XMConvertToDegrees() is also present to go the other way.

The Initialize() method demonstrates the retrieval of the mouse and keyboard services from the service container. Note that these services potentially might not exist. In that case, the returned value is NULL. This component is written so that it doesn’t require the keyboard or mouse to function. Otherwise, you want to assert that the objects are present in the service container.

The Update() method contains the bulk of the implementation. It first tests whether the W, S, A, and D keys are pressed; if so, it saves off the value of 1 or -1 to denote the direction of movement. A keyboard is digital—its keys are either pressed or not—and therefore cannot express a variable quantity such as an analog trigger or thumbstick on a gamepad. Thus, the mMovementRate class member defines the magnitude of the movement (although it would be reasonable to scale the magnitude by the length of time the key was held down).

Next, the rotation amounts are collected from the mouse (if the left mouse button is held down). These amounts are multiplied by the mRotationRate member and the elapsed time of the frame. Notice that the rotationAmount variable is loaded into an XMVECTOR object, before the multiplication, to take advantage of SIMD operations. The elapsed time element is included to make the computation independent of the frame rate.

With the rotation amounts in hand, you construct the yaw and pitch rotation matrices you’ll apply to the camera. The pitch matrix is created with a call to XMMatrixRotationAxis(), which builds a matrix that rotates around an arbitrary axis. In this case, the axis is the camera’s right vector. Note the XMVectorGetY() function used in the second argument to the XMMatrixRotationAxis() call. This retrieves the Y component from the opaque XMVECTOR object. The yaw matrix is created through XMMatrixRotationY(), which builds a rotation matrix around the y-axis. The DirectXMath library has analogous calls for building rotation matrices around the x- and z-axes. The yaw and pitch matrices are concatenated through XMMatrixMultiply() before being passed to the Camera::ApplyRotation() method.

Next, the movement amount is modulated by the mMovementRate member and the elapsed time and is stored in a vector. Then the strafe vector is calculated by multiplying the camera’s right vector by the X component of the movement vector. This quantity is added to the camera’s position. The process is repeated for forward movement, and the final position is written back to the Camera::mPosition member.

Summary

In this chapter, you developed a base component to support common aspects of a virtual camera. Then you extended this class to create a first-person camera controlled by the mouse and keyboard. Along the way, you exercised a bit more of the component and services scaffolding that you created in the last chapter, and you revisited some 3D camera theory and DirectX math usage.

All this work is about to pay off. In the next chapter, you begin rendering 3D content to the screen.

Exercise

1. Create an orbit camera similar to the one found in NVIDIA FX Composer. Note: Visualizing the output of your camera will be difficult without any 3D rendering (which the next chapter covers). You can use the SpriteBatch/SpriteFont system to output your camera’s data or employ the Grid component on the companion website. This component renders a customizable reference grid at the world origin.

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

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