第十三章 Cameras
本章,主要讲解如何创建一个可复用的camera系统用于显示3D场景。首先创建一个基类用于支持通用的camera功能,然后扩展该基类,在此基础上创建一个第一人称的camera(first-person camera),可以通过鼠标和键盘进行控制。
列表13.1列出了Camera类的声明代码,其中包含了用于表示这些输入和输出的成员变量。该类包含有完整的实现代码,可以在应用中直接使用。实际上,在下一章才会使用这个camera渲染第一个3D场景。但是,Camera类的真正目的是作为一个基类,而且不包含特定的camera运动方式,特定的行为由Camera类的派生类去实现。
A Base Camera Component
关于虚拟camera的主题穿插在本书的方方面面,而且没有虚拟camera就无法显示3D场景。但是,并没有一个放之四海而皆准的camera可以满足所有应用程序的需求。比如,你可能需要一个类似于NVIDIA FX Composer中的orbit camera(沿着转道旋转的camera)。 又或者创建一个使用第一人称camera的应用程序,在这种程序中可以使用鼠标控制pitch(俯仰)和yaw(偏航),使用键盘的W,A,S和D键控制移动。还可能创建一个2.5D平台游戏(这是一种卷轴类的游戏,也是通过3D渲染但是使用一种固定坐标轴的camera)或者一种动作游戏,这种游戏类似于把第三人称camera扛在肩上,跟随人物移动并能一起跳跃。你可以实现多种类型的cameras,但是所有camera都是需要使用同样的基础功能,这些基础功能不依赖于任何具体的camera行为方式。这一节,主要开发一个旨在用途基类的通常camera component。然后扩展该基类,创建一个第一人称camera用于后面的示例中。Camera Theory Revisited
回顾一下第2章,“A 3D/Math Primer”,关于cameras的讨论。其中介绍了camera的视域体的属性,一个视域体类似于一个切断尖顶的金字塔,只有位于其内部的objects才是可见的。一个视域体由3个部分组成:camera的坐标位置(位于world space),描述camera朝向的向量,以及一个描述方向朝上的向量。Camera还有一此额外的属性,包括垂直方向的FOV(field of view),渲染视图的aspect ratio(宽除以高),以及近裁剪面和远裁剪面的距离。可以把这些属性作为camera的输入。同样,之前还讨论了使用这些属性可以定义一些变换矩阵,用于把objects变换到view space和projection space。因此,可以把camera的输出作为view和projection矩阵,或者view-projection的组合矩阵。列表13.1列出了Camera类的声明代码,其中包含了用于表示这些输入和输出的成员变量。该类包含有完整的实现代码,可以在应用中直接使用。实际上,在下一章才会使用这个camera渲染第一个3D场景。但是,Camera类的真正目的是作为一个基类,而且不包含特定的camera运动方式,特定的行为由Camera类的派生类去实现。
列表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);
};
}
在讲解Camera类的实现代码之前,先分析一下类的结构。如列表13.1的代码所示,Camera类继承自一个game component,因此可以通过Game基类进行初始化并更新,而不需要显示调用Camera类的Initialize()和Update()函数。相应的,只需要简单地在Game::mComponents成员变量中增加一个Camera类的实例。Camera类还可以作为service container中一种service,用于任何需要在场景中绘制3D objects的components。
Camera类中包含了一些成员变量用于表示FOV(filed of view),aspect ratio,远裁剪和近裁剪面的距离,还有一个构造函数使用这些值作为参数。在默认构造函数中,也使用了这些数值,这些值存储对应的静态成员变量中:DefaultFiledOfView,DefaultAspectRatio,DefaultNearPlaneDistance以及DefaultFarPlaneDistance。
用于定义camera在三维空间中的方向的成员变量为:mDirection,mUp和mRight。这三个向量相互正交,并保持一致的旋转。(更具体地说,就是旋转direction和up向量,再对两个向量执行cross product得到right向量。)另一个成员变量mPosition存储了camera的坐标位置。
Camera的输出由成员变量mViewMatrix和mProjectionMatrix表示。在列出Camera的实现代码之后,再讨论如何使用Direct3D的相关函数来计算这两个变量值。列表13.2列出了Camera类实现代码的一部分。为简便起见,省略了用于设置或修改变量值的只有一行代码的函数代码。在本书的配套网站上提供了完成实现代码。
列表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()
{
}
Camera::Camera(Game& game, float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance)
: GameComponent(game),
mFieldOfView(fieldOfView), mAspectRatio(aspectRatio), mNearPlaneDistance(nearPlaneDistance), mFarPlaneDistance(farPlaneDistance),
mPosition(), mDirection(), mUp(), mRight(), mViewMatrix(), mProjectionMatrix()
{
}
Camera::~Camera()
{
}
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
首先,分析在Camera类的实现代码中所使用的DirectXMath相关函数。例如,在Camera::ViewProjectionMatrix()函数中,首先把成员变量mViewMatrix和mProjectionMatrix分别加载到两个XMMATRIX变量中,然后调用XMMatrixMultiply()函数对这两个变量相乘。回顾一下第2章讨论的DirectXMath,以及使用SIMD指令所带来的性能提升。矩阵和向量运算都应该使用SIMD类型的变量,但SIMD类型使用16-byte(16字节)对齐的格式,并不适合作为类的成员变量的类型。因此,在Camera类中成员变量view和projection matrices使用XMFLOAT4X4类型,然后把这两个变量加载到XMMATRIX类型的变量中。同样地,使用XMFLOAT3类型作为成员变量类型,然后加载到XMVECTOR类型的变量中进行计算。相反,如果需要把SIMD类型的变量保存到对应的类成员变量中,需要调用对应的存储函数,比如XMStoreFloat3()和XMStoreFloat4X4()。在Camera::UpdateViewMatrix()和Camere::ApplyRotation()函数中分别使用了这两个函数。
注意
一种替代DirectXMath的Load和Store函数的方法是,使用__declspec(align(16))把类和结构体沿着16字节边界对齐。关于数据对齐的更多描述请查看http://msdn.microsoft.com/en-us/library/83ythb65.aspx.
The Reset() Method
在Camera::Reset()函数中,首先把表示camera的坐标位置以及三个方向的向量的所有成员变量设置为合理的初始值,然后调用Camera::UpdateViewMatrix()函数。只要camera的坐标位置或三个方向的向量发生变化,必须更新成员变量mViewMatrix的值。与Camera中大部分函数一样,Reset()函数也是虚函数,可以在Camera的派生类中重新实现。Reset()函数中还使用了一个Vector3Helpter类,该类中包含了大量通用的XMFLOAT3类型的静态成员变量。 类似的,Vector2Helper和Vector4Helper分别表示2D和4D向量。在本书的配套网站上提供了VectorHelper.h头文件,其中包含了这些辅助类。
The UpdateViewMatrix() Method
Camera::UpdateViewMatrix()函数中使用DirectXMath库的XMMatrixLookToRH()函数,创建一个用于右手坐标系的view matrix。对应的XMMatrixLookToLH()函数则创建一个用于左手坐标系的view matrix。这两个都包含三个XMVECTOR类型的参数,分别为camera的postion,direction和up向量。这三个参数可以从Camera类中对应的成员变量加载。在Camear::Update()函数中,每一帧都会调用一次UpdateViewMatrix()函数。可以考虑对此做一点点优化,如果camera的position和orientation没有改变,就可以在不执行view matrix的计算,这种情况就是一种“dirty”状态(类似于Windows Paint的脏区域)。The UpdateProjectionMatrix() Method
与计算view matrix的函数一样,DirectXMath库中还提供了用于计算projection matrix的函数XMMatrixPerspectiveFovRH()。该函数有四个参数,分别为FOV(filed of view),aspect ratio,近裁剪面,和远裁剪面的距离。与该函数对应的还有一个用于左手坐标系的XMMatrixPerspectiveFovLH()函数。与计算view matrix不同的是,projection matrix不需要每一帧都计算一次。实际上,projection matrix通常只计算一次(在初始化camera时),并且在整个程序执行过程中都不会修改。这也是本书所使用的方法。其中,在Camera类中相关的成员变量的访问权限控制都是protected,而且没有提供public设置接口。但是,在一个派生类中可以修改这些成员(因为基类的protected成员变量在派生类可以访问),可以在初始化之后再修改projection matrix。
The ApplyRotation() Method
尽管在Camera基类中没有提供camera的隐式移动方式,但是包含了一个公有的ApplyRotation函数。通过该函数,可以使用一个rotation matrix(旋转矩阵)计算camera的orientation。首先,使用rotation matrix对camera的direction和up向量执行变换,然后把这两个向量进行cross product得到right向量。令人奇怪的是,在计算得到right向量之后,马上又进行了一次cross product运算,通过计算right向量和direction向量的cross product得到up向量。这一步的目的是为了消除所有的计算误差,并使得这三个向量保持相互正交。需要注意的是,调用ApplyRotation()函数执行的任何变换都不会修改camera的坐标位置。但是,可以使用各个版本的SetPosition()重载函数进行修改。