圖形引擎(四):創建自定義的Camera
上一篇文章中,已經討論瞭如何在應用程序中添加組件,並完成了對輸入設備如鍵盤和鼠標的響應操作,本篇文章中將講解如何使用鼠標和鍵盤控制場景。任何一個3D場景中都需要使用一個虛擬Camera用於顯示,根據Camera的座標位置和觀察方向的不同,可以觀察場景的不同角度。爲此,我們將會創建一個基礎的Camera類,包含了Camera的基礎功能屬性,然後在此基礎上完成一個第一人稱的First-Person Camera類。作爲練習,在創建一個類似於NVIDIA FX Composer中的Orbit Camera。
關於Camera基類,以及FirstPersonCamera類的詳細講解,請查看另一篇博客。在這裏,重點講解如何創建一個Orbit Camera。
Orbit Camera
Orbit Camera是指Camera圍繞某個座標軸,沿着對應的軌道運行,並保持觀察方向指向世界座標空間的原點。具體地說,就是在世界座標空間中,以原點爲中心,Camera與中心的距離作爲半徑,然後圍繞與camera的Right或Up方向平行的軸進行旋轉,旋轉所經過的路徑就是運行的軌道。
在第一人稱Camera中,已經計算了俯仰和偏航,詳見http://blog.csdn.net/chenjinxian_3d/article/details/51946202。俯仰是指Camera圍繞Right方向軸旋轉一定的角度,而偏航是圍繞Up方向軸旋轉一定的角度。在些基礎上,可以把俯仰的角度作爲Orbit
Camera圍繞世界座標空間與Camera的Right方向平行的軸的旋轉角度,把偏航的角度作爲Orbit Camera圍繞世界座標空間與Camera的Up方向平行的軸的旋轉角度。因此Orbit Camera圍繞軌道運行一定角度後的座標位置就可以通過把Camera的Postion與旋轉矩陣相乘得到。由於Camera的座標位置改變後,Camera的觀察方向需要保持指向世界座標空間的原點,因此在第一人稱Camera中執行的旋轉操作在Orbit Camera仍然需要。
Orbit Camera的代碼如下
#pragma once
#include "Camera.h"
namespace Library
{
class Keyboard;
class Mouse;
class OrbitCamera : public Camera
{
RTTI_DECLARATIONS(OrbitCamera, Camera)
public:
OrbitCamera(Game& game);
OrbitCamera(Game& game, float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance);
virtual ~OrbitCamera();
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:
OrbitCamera(const OrbitCamera& rhs);
OrbitCamera& operator=(const OrbitCamera& rhs);
};
}
#include "OrbitCamera.h"
#include "Game.h"
#include "GameTime.h"
#include "Keyboard.h"
#include "Mouse.h"
#include "VectorHelper.h"
namespace Library
{
RTTI_DEFINITIONS(OrbitCamera)
const float OrbitCamera::DefaultRotationRate = XMConvertToRadians(1.0f);
const float OrbitCamera::DefaultMovementRate = 10.0f;
const float OrbitCamera::DefaultMouseSensitivity = 100.0f;
OrbitCamera::OrbitCamera(Game& game)
: Camera(game), mKeyboard(nullptr), mMouse(nullptr),
mMouseSensitivity(DefaultMouseSensitivity), mRotationRate(DefaultRotationRate), mMovementRate(DefaultMovementRate)
{
}
OrbitCamera::OrbitCamera(Game& game, float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance)
: Camera(game, fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance), mKeyboard(nullptr), mMouse(nullptr),
mMouseSensitivity(DefaultMouseSensitivity), mRotationRate(DefaultRotationRate), mMovementRate(DefaultMovementRate)
{
}
OrbitCamera::~OrbitCamera()
{
mKeyboard = nullptr;
mMouse = nullptr;
}
const Keyboard& OrbitCamera::GetKeyboard() const
{
return *mKeyboard;
}
void OrbitCamera::SetKeyboard(Keyboard& keyboard)
{
mKeyboard = &keyboard;
}
const Mouse& OrbitCamera::GetMouse() const
{
return *mMouse;
}
void OrbitCamera::SetMouse(Mouse& mouse)
{
mMouse = &mouse;
}
float&OrbitCamera:: MouseSensitivity()
{
return mMouseSensitivity;
}
float& OrbitCamera::RotationRate()
{
return mRotationRate;
}
float& OrbitCamera::MovementRate()
{
return mMovementRate;
}
void OrbitCamera::Initialize()
{
mKeyboard = (Keyboard*)mGame->Services().GetService(Keyboard::TypeIdClass());
mMouse = (Mouse*)mGame->Services().GetService(Mouse::TypeIdClass());
Camera::Initialize();
}
void OrbitCamera::Update(const GameTime& gameTime)
{
XMFLOAT2 movementAmount = Vector2Helper::Zero;
XMFLOAT2 rotationAmount = Vector2Helper::Zero;
if (mMouse != nullptr && mKeyboard != nullptr)
{
LPDIMOUSESTATE mouseState = mMouse->CurrentState();
if (mMouse->IsButtonHeldDown(MouseButtonsLeft) && mKeyboard->IsKeyHeldDown(DIK_LMENU))
{
rotationAmount.x = -mouseState->lX * mMouseSensitivity;
rotationAmount.y = -mouseState->lY * mMouseSensitivity;
}
else
{
movementAmount.y = static_cast<float>(mouseState->lZ);
}
}
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));
XMMATRIX transform = XMMatrixMultiply(pitchMatrix, yawMatrix);
ApplyRotation(transform);
XMVECTOR position = XMLoadFloat3(&mPosition);
position = XMVector3TransformNormal(position, transform);
XMVECTOR movement = XMLoadFloat2(&movementAmount) * mMovementRate * elapsedTime;
XMVECTOR forward = XMLoadFloat3(&mDirection) * XMVectorGetY(movement);
position += forward;
XMStoreFloat3(&mPosition, position);
Camera::Update(gameTime);
}
}
其中,除了Update函數其他的與FirstPersonCamera基本一樣,在Update函數中只是對Camera的Position進行了旋轉變換。
Camera示例
創建了Orbit Camera之後,就可以在RenderingGame中使用了。在下面的示例程序中,爲了更好的演示兩種Camera,我們在代碼中同時添加兩種Camera,並通過鍵盤控制使用哪一種Camera。更新後的RenderingGame類的聲明和實現代碼如下:
#pragma once
#include "Common.h"
#include "Game.h"
using namespace Library;
namespace DirectX
{
class SpriteBatch;
class SpriteFont;
}
namespace Library
{
class FpsComponent;
class Keyboard;
class Mouse;
class FirstPersonCamera;
class OrbitCamera;
class Grid;
}
namespace Rendering
{
class RenderingGame : public Game
{
public:
RenderingGame(HINSTANCE instance, const std::wstring& windowClass, const std::wstring& windowTitle, int showCommand);
~RenderingGame();
virtual void Initialize() override;
virtual void Update(const GameTime& gameTime) override;
virtual void Draw(const GameTime& gameTime) override;
protected:
virtual void Shutdown() override;
private:
static const XMVECTORF32 BackgroundColor;
FpsComponent* mFpsComponent;
FirstPersonCamera * mFirstPersonCamera;
OrbitCamera * mOrbitCamera;
Grid* mGrid;
LPDIRECTINPUT8 mDirectInput;
Keyboard* mKeyboard;
Mouse* mMouse;
SpriteBatch* mSpriteBatch;
SpriteFont* mSpriteFont;
XMFLOAT2 mMouseTextPosition;
XMFLOAT2 mCameraTextPosition;
XMFLOAT2 mTipTextPosition;
};
}
#include "RenderingGame.h"
#include <SpriteBatch.h>
#include <SpriteFont.h>
#include "GameException.h"
#include "Keyboard.h"
#include "Mouse.h"
#include "FpsComponent.h"
#include "FirstPersonCamera.h"
#include "OrbitCamera.h"
#include "Grid.h"
#include "Utility.h"
#include <sstream>
#include <string>
#include <iomanip>
namespace Rendering
{
const XMVECTORF32 RenderingGame::BackgroundColor = { 0.392f, 0.584f, 0.929f, 1.0f };
RenderingGame::RenderingGame(HINSTANCE instance, const std::wstring& windowClass, const std::wstring& windowTitle, int showCommand)
: Game(instance, windowClass, windowTitle, showCommand),
mFpsComponent(nullptr), mFirstPersonCamera(nullptr), mOrbitCamera(nullptr), mGrid(nullptr),
mDirectInput(nullptr), mKeyboard(nullptr), mMouse(nullptr),
mSpriteBatch(nullptr), mSpriteFont(nullptr),
mMouseTextPosition(0.0f, 20.0f), mCameraTextPosition(0.0f, mScreenHeight - 40.0f), mTipTextPosition(0.0f, mScreenHeight - 80.0f)
{
mDepthStencilBufferEnabled = true;
mMultiSamplingEnabled = true;
// mIsFullScreen = true;
}
RenderingGame::~RenderingGame()
{
}
void RenderingGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
mKeyboard = new Keyboard(*this, mDirectInput);
mComponents.push_back(mKeyboard);
mServices.AddService(Keyboard::TypeIdClass(), mKeyboard);
mMouse = new Mouse(*this, mDirectInput);
mComponents.push_back(mMouse);
mServices.AddService(Mouse::TypeIdClass(), mMouse);
mFpsComponent = new FpsComponent(*this);
mComponents.push_back(mFpsComponent);
mFirstPersonCamera = new FirstPersonCamera(*this);
mComponents.push_back(mFirstPersonCamera);
mServices.AddService(Camera::TypeIdClass(), mFirstPersonCamera);
mOrbitCamera = new OrbitCamera(*this);
mComponents.push_back(mOrbitCamera);
mServices.AddService(Camera::TypeIdClass(), mOrbitCamera);
mGrid = new Grid(*this, *mFirstPersonCamera);
mComponents.push_back(mGrid);
SetCurrentDirectory(Utility::ExecutableDirectory().c_str());
mSpriteBatch = new SpriteBatch(mDirect3DDeviceContext);
mSpriteFont = new SpriteFont(mDirect3DDevice, L"Assets\\Fonts\\Arial_14_Regular.spritefont");
Game::Initialize();
if (mIsFullScreen)
{
mCameraTextPosition = XMFLOAT2(0.0f, mScreenHeight - 40.0f);
mTipTextPosition = XMFLOAT2(0.0f, mScreenHeight - 80.0f);
}
}
void RenderingGame::Shutdown()
{
DeleteObject(mKeyboard);
DeleteObject(mMouse);
DeleteObject(mFpsComponent);
DeleteObject(mFirstPersonCamera);
DeleteObject(mOrbitCamera);
DeleteObject(mGrid);
DeleteObject(mSpriteFont);
DeleteObject(mSpriteBatch);
ReleaseObject(mDirectInput);
Game::Shutdown();
}
void RenderingGame::Update(const GameTime &gameTime)
{
if (mKeyboard->WasKeyPressedThisFrame(DIK_ESCAPE))
{
Exit();
}
if (mKeyboard->WasKeyPressedThisFrame(DIK_F))
{
mOrbitCamera->SetEnabled(false);
mFirstPersonCamera->SetEnabled(true);
mGrid->SetCamera(mFirstPersonCamera);
}
else if (mKeyboard->WasKeyPressedThisFrame(DIK_O))
{
mFirstPersonCamera->SetEnabled(false);
mOrbitCamera->SetEnabled(true);
mGrid->SetCamera(mOrbitCamera);
}
Game::Update(gameTime);
}
void RenderingGame::Draw(const GameTime &gameTime)
{
mDirect3DDeviceContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&BackgroundColor));
mDirect3DDeviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
Game::Draw(gameTime);
mSpriteBatch->Begin();
std::wostringstream mouseLabel;
mouseLabel << L"Mouse Position: " << mMouse->X() << L", " << mMouse->Y() << L" Mouse Wheel: " << mMouse->Wheel();
mSpriteFont->DrawString(mSpriteBatch, mouseLabel.str().c_str(), mMouseTextPosition);
std::wostringstream cameraLabel;
cameraLabel << std::setprecision(4) <<
L"Camera Position: " << mFirstPersonCamera->Position().x << L", " << mFirstPersonCamera->Position().y << L", " << mFirstPersonCamera->Position().z <<
L" Camera Direction: " << mFirstPersonCamera->Direction().x << L", " << mFirstPersonCamera->Direction().y << L", " << mFirstPersonCamera->Direction().z;
mSpriteFont->DrawString(mSpriteBatch, cameraLabel.str().c_str(), mCameraTextPosition);
std::wstring tipLabel = L"Switch Camera: (F)Use first person camera (O)Use orbit camera";
mSpriteFont->DrawString(mSpriteBatch, tipLabel.c_str(), mTipTextPosition);
mSpriteBatch->End();
HRESULT hr = mSwapChain->Present(0, 0);
if (FAILED(hr))
{
throw GameException("IDXGISwapChain::Present() failed.", hr);
}
}
}
其中有幾點需要注意:
1.除了兩種Camera之外,還增加一個Grid,Grid組件主要是在場景中添加了一個參考表格,用於用於更好的顯示Camera。關於Grid類使用的相關知識,後面在Effect章節會詳細討論。
2.增加了用於顯示切換Camera的提示語,以及Camera的相關信息的文字輸出。
3.通常情況下每一種Camera都是相關獨立的,具有獨立的座標和方向,在該示例了,由於需要在兩種Camera之間進行切換,因此需要使用同一個座標和方向。爲此,修改Camera基類,把座標和方向相關的變量設爲靜態成員變量:
static XMFLOAT3 mPosition;
static XMFLOAT3 mDirection;
static XMFLOAT3 mUp;
static XMFLOAT3 mRight;
static XMFLOAT3 mDirection;
static XMFLOAT3 mUp;
static XMFLOAT3 mRight;
這樣,在兩種Camera中都使用同樣的變量值,切換Camera時就可以保持觀察座標和方向保持不變。
圖形引擎第四步的完整工程代碼: