DirectX11 With Windows SDK--10 攝像機類

前言

DirectX11 With Windows SDK完整目錄:http://www.cnblogs.com/X-Jun/p/9028764.htmlhtml

因爲考慮後續的項目須要有一個比較好的演示環境,所以這裏將先從攝像機這個專題入手。在這以前,須要複習一下有關世界矩陣和觀察矩陣的內容。git

DirectX11 With Windows SDK完整目錄github

Github項目源碼數組

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。app

世界矩陣和觀察矩陣

若已知物體所在位置\(\mathbf{Q} = (Q_{x}, Q_{y}, Q_{z})\)以及三個互相垂直的座標軸 \(\mathbf{u} = (u_{x}, u_{y}, u_{z})\), \(\mathbf{v} = (v_{x}, v_{y}, v_{z})\), \(\mathbf{w} = (w_{x}, w_{y}, w_{z})\),則咱們能夠獲得對應的世界矩陣:
\[ \mathbf{W}=\begin{bmatrix} u_{x} & u_{y} & u_{z} & 0 \\ v_{x} & v_{y} & v_{z} & 0 \\ w_{x} & w_{y} & w_{z} & 0 \\ Q_{x} & Q_{y} & Q_{z} & 1 \end{bmatrix}\]
該矩陣的應用有兩種解釋方式:ide

  1. 將物體從世界座標系的原點搬移到世界矩陣對應的位置,並按其座標軸作對應朝向和大小的調整
  2. 通過世界變化後物體已經在世界座標系的對應位置,其實是作從物體座標系到世界座標系的變換

然而如今咱們須要作的是從世界座標系轉換到觀察空間座標系,若是把攝像機看作物體的話,則實際上作的至關因而世界矩陣的逆變換,從世界座標系來到了攝像機的局部座標系(右方向爲X軸,上方向爲Y軸,目視方向爲Z軸),即\(\mathbf{V}=\mathbf{(RT)}^{-1}=\mathbf{T}^{-1}\mathbf{R}^{-1}=\mathbf{T}^{-1}\mathbf{R}^{T}\)函數

\[ \mathbf{V}=\begin{bmatrix} u_{x} & v_{x} & w_{x} & 0 \\ u_{y} & v_{y} & w_{y} & 0 \\ u_{z} & v_{z} & w_{z} & 0 \\ -\mathbf{Q}\cdot\mathbf{u} & -\mathbf{Q}\cdot\mathbf{v} & -\mathbf{Q}\cdot\mathbf{w} & 1 \end{bmatrix}\]佈局

攝像機

第一人稱/自由視角攝像機和第三人稱攝像機在元素構成上是有部分相同的地方,所以在這裏能夠提煉出它們相同的部分來實現攝像機的抽象基類性能

攝像機抽象基類

Camera類的定義以下:spa

class Camera
{
public:
    Camera();
    virtual ~Camera() = 0;

    // 獲取攝像機位置
    DirectX::XMVECTOR GetPositionXM() const;
    DirectX::XMFLOAT3 GetPosition() const;

    // 獲取攝像機的座標軸向量
    DirectX::XMVECTOR GetRightXM() const;
    DirectX::XMFLOAT3 GetRight() const;
    DirectX::XMVECTOR GetUpXM() const;
    DirectX::XMFLOAT3 GetUp() const;
    DirectX::XMVECTOR GetLookXM() const;
    DirectX::XMFLOAT3 GetLook() const;

    // 獲取視錐體信息
    float GetNearWindowWidth() const;
    float GetNearWindowHeight() const;
    float GetFarWindowWidth() const;
    float GetFarWindowHeight() const;

    // 獲取矩陣
    DirectX::XMMATRIX GetViewXM() const;
    DirectX::XMMATRIX GetProjXM() const;
    DirectX::XMMATRIX GetViewProjXM() const;

    // 獲取視口
    D3D11_VIEWPORT GetViewPort() const;


    // 設置視錐體
    void SetFrustum(float fovY, float aspect, float nearZ, float farZ);

    // 設置視口
    void SetViewPort(const D3D11_VIEWPORT& viewPort);
    void SetViewPort(float topLeftX, float topLeftY, float width, float height, float minDepth = 0.0f, float maxDepth = 1.0f);

    // 更新觀察矩陣
    virtual void UpdateViewMatrix() = 0;
protected:
    // 攝像機的觀察空間座標系對應在世界座標系中的表示
    DirectX::XMFLOAT3 m_Position;
    DirectX::XMFLOAT3 m_Right;
    DirectX::XMFLOAT3 m_Up;
    DirectX::XMFLOAT3 m_Look;
    
    // 視錐體屬性
    float m_NearZ;
    float m_FarZ;
    float m_Aspect;
    float m_FovY;
    float m_NearWindowHeight;
    float m_FarWindowHeight;

    // 觀察矩陣和透視投影矩陣
    DirectX::XMFLOAT4X4 m_View;
    DirectX::XMFLOAT4X4 m_Proj;

    // 當前視口
    D3D11_VIEWPORT m_ViewPort;

};

能夠看到,不管是什麼類型的攝像機,都必定須要包含觀察矩陣、投影矩陣以及設置這兩個座標系所須要的一些相關信息。這裏面觀察矩陣的更新是虛方法,是由於第一人稱/自由視角攝像機實現和第三人稱的不一樣。

這裏只列出視錐體信息的獲取方法:

float Camera::GetNearWindowWidth() const
{
    return m_Aspect * m_NearWindowHeight;
}

float Camera::GetNearWindowHeight() const
{
    return m_NearWindowHeight;
}

float Camera::GetFarWindowWidth() const
{
    return m_Aspect * m_FarWindowHeight;
}

float Camera::GetFarWindowHeight() const
{
    return m_FarWindowHeight;
}

第一人稱/自由視角攝像機

FirstPersonCamera類的定義以下:

class FirstPersonCamera : public Camera
{
public:
    FirstPersonCamera();
    ~FirstPersonCamera() override;

    // 設置攝像機位置
    void SetPosition(float x, float y, float z);
    void SetPosition(const DirectX::XMFLOAT3& v);
    // 設置攝像機的朝向
    void XM_CALLCONV LookAt(DirectX::FXMVECTOR pos, DirectX::FXMVECTOR target, DirectX::FXMVECTOR up);
    void LookAt(const DirectX::XMFLOAT3& pos, const DirectX::XMFLOAT3& target,const DirectX::XMFLOAT3& up);
    void XM_CALLCONV LookTo(DirectX::FXMVECTOR pos, DirectX::FXMVECTOR to, DirectX::FXMVECTOR up);
    void LookTo(const DirectX::XMFLOAT3& pos, const DirectX::XMFLOAT3& to, const DirectX::XMFLOAT3& up);
    // 平移
    void Strafe(float d);
    // 直行(平面移動)
    void Walk(float d);
    // 前進(朝前向移動)
    void MoveForward(float d);
    // 上下觀察
    void Pitch(float rad);
    // 左右觀察
    void RotateY(float rad);


    // 更新觀察矩陣
    void UpdateViewMatrix() override;
};

該第一人稱攝像機沒有實現碰撞檢測,它具備以下功能:

  1. 設置攝像機的朝向、位置
  2. 朝攝像機的正前方進行向前/向後移動(自由視角)
  3. 在水平地面上向前/向後移動(第一人稱視角)
  4. 左/右平移
  5. 視野左/右旋轉(繞Y軸)
  6. 視野上/下旋轉(繞攝像機的右方向軸),並限制了旋轉角度防止旋轉角度過大

具體實現以下:

void FirstPersonCamera::SetPosition(float x, float y, float z)
{
    SetPosition(XMFLOAT3(x, y, z));
}

void FirstPersonCamera::SetPosition(const DirectX::XMFLOAT3 & v)
{
    m_Position = v;
}

void XM_CALLCONV FirstPersonCamera::LookAt(DirectX::FXMVECTOR pos, DirectX::FXMVECTOR target, DirectX::FXMVECTOR up)
{
    LookTo(pos, target - pos, up);
}

void FirstPersonCamera::LookAt(const DirectX::XMFLOAT3 & pos, const DirectX::XMFLOAT3 & target,const DirectX::XMFLOAT3 & up)
{
    LookAt(XMLoadFloat3(&pos), XMLoadFloat3(&target), XMLoadFloat3(&up));
}

void XM_CALLCONV FirstPersonCamera::LookTo(DirectX::FXMVECTOR pos, DirectX::FXMVECTOR to, DirectX::FXMVECTOR up)
{
    XMVECTOR L = XMVector3Normalize(to);
    XMVECTOR R = XMVector3Normalize(XMVector3Cross(up, L));
    XMVECTOR U = XMVector3Cross(L, R);

    XMStoreFloat3(&m_Position, pos);
    XMStoreFloat3(&m_Look, L);
    XMStoreFloat3(&m_Right, R);
    XMStoreFloat3(&m_Up, U);
}

void FirstPersonCamera::LookTo(const DirectX::XMFLOAT3 & pos, const DirectX::XMFLOAT3 & to, const DirectX::XMFLOAT3 & up)
{
    LookTo(XMLoadFloat3(&pos), XMLoadFloat3(&to), XMLoadFloat3(&up));
}

void FirstPersonCamera::Strafe(float d)
{
    XMVECTOR Pos = XMLoadFloat3(&m_Position);
    XMVECTOR Right = XMLoadFloat3(&m_Right);
    XMVECTOR Dist = XMVectorReplicate(d);
    // DestPos = Dist * Right + SrcPos
    XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Right, Pos));
}

void FirstPersonCamera::Walk(float d)
{
    XMVECTOR Pos = XMLoadFloat3(&m_Position);
    XMVECTOR Right = XMLoadFloat3(&m_Right);
    XMVECTOR Up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
    XMVECTOR Front = XMVector3Normalize(XMVector3Cross(Right, Up));
    XMVECTOR Dist = XMVectorReplicate(d);
    // DestPos = Dist * Front + SrcPos
    XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Front, Pos));
}

void FirstPersonCamera::MoveForward(float d)
{
    XMVECTOR Pos = XMLoadFloat3(&m_Position);
    XMVECTOR Look = XMLoadFloat3(&m_Look);
    XMVECTOR Dist = XMVectorReplicate(d);
    // DestPos = Dist * Look + SrcPos
    XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Look, Pos));
}

void FirstPersonCamera::Pitch(float rad)
{
    XMMATRIX R = XMMatrixRotationAxis(XMLoadFloat3(&m_Right), rad);
    XMVECTOR Up = XMVector3TransformNormal(XMLoadFloat3(&m_Up), R);
    XMVECTOR Look = XMVector3TransformNormal(XMLoadFloat3(&m_Look), R);
    float cosPhi = XMVectorGetY(Look);
    // 將上下視野角度Phi限制在[2pi/9, 7pi/9],
    // 即餘弦值[-cos(2pi/9), cos(2pi/9)]之間
    if (fabs(cosPhi) > cosf(XM_2PI / 9))
        return;
    
    XMStoreFloat3(&m_Up, Up);
    XMStoreFloat3(&m_Look, Look);
}

void FirstPersonCamera::RotateY(float rad)
{
    XMMATRIX R = XMMatrixRotationY(rad);

    XMStoreFloat3(&m_Right, XMVector3TransformNormal(XMLoadFloat3(&m_Right), R));
    XMStoreFloat3(&m_Up, XMVector3TransformNormal(XMLoadFloat3(&m_Up), R));
    XMStoreFloat3(&m_Look, XMVector3TransformNormal(XMLoadFloat3(&m_Look), R));
}

其中上下視野角度Phi、觀察軸Y值有以下對應關係:
\[L_{y} = cos(\Phi)\]
Phi爲弧度0的時候至關於豎直向上看,Phi爲弧度pi時至關於豎直向下看。在本例中將視野角度Phi限制在弧度[2pi/9, 7pi/9]

構造觀察矩陣

FirstPersonCamera::UpdateViewMatrix方法首先須要從新規格化、正交化攝像機的右方向軸、上方向軸和前方向軸,而後計算剩餘的部分以填充觀察矩陣:

void FirstPersonCamera::UpdateViewMatrix()
{
    XMVECTOR R = XMLoadFloat3(&m_Right);
    XMVECTOR U = XMLoadFloat3(&m_Up);
    XMVECTOR L = XMLoadFloat3(&m_Look);
    XMVECTOR P = XMLoadFloat3(&m_Position);

    // 保持攝像機的軸互爲正交,且長度都爲1
    L = XMVector3Normalize(L);
    U = XMVector3Normalize(XMVector3Cross(L, R));

    // U, L已經正交化,須要計算對應叉乘獲得R
    R = XMVector3Cross(U, L);

    // 填充觀察矩陣
    float x = -XMVectorGetX(XMVector3Dot(P, R));
    float y = -XMVectorGetX(XMVector3Dot(P, U));
    float z = -XMVectorGetX(XMVector3Dot(P, L));

    XMStoreFloat3(&m_Right, R);
    XMStoreFloat3(&m_Up, U);
    XMStoreFloat3(&m_Look, L);

    m_View = {
        m_Right.x, m_Up.x, m_Look.x, 0.0f,
        m_Right.y, m_Up.y, m_Look.y, 0.0f,
        m_Right.z, m_Up.z, m_Look.z, 0.0f,
        x, y, z, 1.0f
    };
}

第三人稱攝像機

ThirdPersonCamera類的定義以下:

class ThirdPersonCamera : public Camera
{
public:
    ThirdPersonCamera();
    ~ThirdPersonCamera() override;

    // 獲取當前跟蹤物體的位置
    DirectX::XMFLOAT3 GetTargetPosition() const;
    // 獲取與物體的距離
    float GetDistance() const;
    // 獲取繞X軸的旋轉方向
    float GetRotationX() const;
    // 獲取繞Y軸的旋轉方向
    float GetRotationY() const;
    // 繞物體垂直旋轉(注意上下視野角度Phi限制在[pi/6, pi/2])
    void RotateX(float rad);
    // 繞物體水平旋轉
    void RotateY(float rad);
    // 拉近物體
    void Approach(float dist);
    // 設置初始繞X軸的弧度(注意上下視野角度Phi限制在[pi/6, pi/2])
    void SetRotationX(float phi);
    // 設置初始繞Y軸的弧度
    void SetRotationY(float theta);
    // 設置並綁定待跟蹤物體的位置
    void SetTarget(const DirectX::XMFLOAT3& target);
    // 設置初始距離
    void SetDistance(float dist);
    // 設置最小最大容許距離
    void SetDistanceMinMax(float minDist, float maxDist);
    // 更新觀察矩陣
    void UpdateViewMatrix() override;

private:
    DirectX::XMFLOAT3 m_Target;
    float m_Distance;
    // 最小容許距離,最大容許距離
    float m_MinDist, m_MaxDist;
    // 以世界座標系爲基準,當前的旋轉角度
    float m_Theta;
    float m_Phi;
};

該第三人稱攝像機一樣沒有實現碰撞檢測,它具備以下功能:

  1. 設置觀察目標的位置
  2. 設置與觀察目標的距離(限制在合理範圍內)
  3. 繞物體進行水平旋轉
  4. 繞物體Y軸進行旋轉

上述部分具體實現以下:

void ThirdPersonCamera::RotateX(float rad)
{
    m_Phi -= rad;
    // 將上下視野角度Phi限制在[pi/6, pi/2],
    // 即餘弦值[0, cos(pi/6)]之間
    if (m_Phi < XM_PI / 6)
        m_Phi = XM_PI / 6;
    else if (m_Phi > XM_PIDIV2)
        m_Phi = XM_PIDIV2;
}

void ThirdPersonCamera::RotateY(float rad)
{
    m_Theta = XMScalarModAngle(m_Theta - rad);
}

void ThirdPersonCamera::Approach(float dist)
{
    m_Distance += dist;
    // 限制距離在[m_MinDist, m_MaxDist]之間
    if (m_Distance < m_MinDist)
        m_Distance = m_MinDist;
    else if (m_Distance > m_MaxDist)
        m_Distance = m_MaxDist;
}

void ThirdPersonCamera::SetRotationX(float phi)
{
    m_Phi = XMScalarModAngle(phi);
    // 將上下視野角度Phi限制在[pi/6, pi/2],
    // 即餘弦值[0, cos(pi/6)]之間
    if (m_Phi < XM_PI / 6)
        m_Phi = XM_PI / 6;
    else if (m_Phi > XM_PIDIV2)
        m_Phi = XM_PIDIV2;
}

void ThirdPersonCamera::SetRotationY(float theta)
{
    m_Theta = XMScalarModAngle(theta);
}

void ThirdPersonCamera::SetTarget(const DirectX::XMFLOAT3 & target)
{
    m_Target = target;
}

void ThirdPersonCamera::SetDistance(float dist)
{
    m_Distance = dist;
}

void ThirdPersonCamera::SetDistanceMinMax(float minDist, float maxDist)
{
    m_MinDist = minDist;
    m_MaxDist = maxDist;
}

球面座標系

要計算攝影機在物體後方的某個具體位置,若是使用下面的公式計算出攝像機位置
\[\mathbf{Q} = \mathbf{T} - dist * \mathbf{L} \]
而後經過XMMatrixLookAtLH函數來獲取觀察矩陣,在運行時會發現旋轉的時候會有不和諧的抖動效果,由於這樣計算出來的攝像機位置有偏差影響。

而使用球面座標系計算出來的攝像機位置會比較平滑,不會看到有抖動效果。

對於右手座標系,球面座標系的公式爲:
\[\begin{cases} x = Rsin(\phi)cos(\theta) \\ y = Rsin(\phi)sin(\theta) \\ z = Rcos(\phi) \end{cases} \]

而對於左手座標系,球面座標系的公式爲:
\[\begin{cases} x = Rsin(\phi)cos(\theta) \\ z = Rsin(\phi)sin(\theta) \\ y = Rcos(\phi) \end{cases} \]

最後將物體座標加上,就能夠獲得攝像機的座標:
\[\begin{cases} Q_{x} = T_{x} + Rsin(\phi)cos(\theta) \\ Q_{z} = T_{y} + Rsin(\phi)sin(\theta) \\ Q_{y} = T_{z} + Rcos(\phi) \end{cases} \]

構造觀察矩陣

ThirdPersonCamera::UpdateViewMatrix方法首先須要計算出攝像機的位置,而後和以前同樣從新規格化、正交化攝像機的右方向軸、上方向軸和前方向軸,最後計算剩餘的部分以填充觀察矩陣:

void ThirdPersonCamera::UpdateViewMatrix()
{
    // 球面座標系
    float x = m_Target.x + m_Distance * sinf(m_Phi) * cosf(m_Theta);
    float z = m_Target.z + m_Distance * sinf(m_Phi) * sinf(m_Theta);
    float y = m_Target.y + m_Distance * cosf(m_Phi);
    m_Position = { x, y, z };
    XMVECTOR P = XMLoadFloat3(&m_Position);
    XMVECTOR L = XMVector3Normalize(XMLoadFloat3(&m_Target) - P);
    XMVECTOR R = XMVector3Normalize(XMVector3Cross(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), L));
    XMVECTOR U = XMVector3Cross(L, R);
    
    // 更新向量
    XMStoreFloat3(&m_Right, R);
    XMStoreFloat3(&m_Up, U);
    XMStoreFloat3(&m_Look, L);

    m_View = {
        m_Right.x, m_Up.x, m_Look.x, 0.0f,
        m_Right.y, m_Up.y, m_Look.y, 0.0f,
        m_Right.z, m_Up.z, m_Look.z, 0.0f,
        -XMVectorGetX(XMVector3Dot(P, R)), -XMVectorGetX(XMVector3Dot(P, U)), -XMVectorGetX(XMVector3Dot(P, L)), 1.0f
    };
}

合理對常量緩衝區進行分塊

因爲項目正在逐漸變得更加龐大,常量緩衝區會頻繁更新,可是每次更新常量緩衝區都必須將整個塊的內容都刷新一遍,若是隻是爲了更新裏面其中一個變量就要進行一次塊的刷新,這樣會致使性能上的損耗。因此將常量緩衝區根據刷新頻率和類別來進行更細緻的分塊,能夠儘量保證每一次更新都不會有變量在進行無心義的刷新。所以HLSL常量緩衝區的變化以下:

cbuffer CBChangesEveryDrawing : register(b0)
{
    matrix g_World;
    matrix g_WorldInvTranspose;
}

cbuffer CBChangesEveryFrame : register(b1)
{
    matrix g_View;
    float3 g_EyePosW;
}

cbuffer CBChangesOnResize : register(b2)
{
    matrix g_Proj;
}

cbuffer CBChangesRarely : register(b3)
{
    DirectionalLight g_DirLight[10];
    PointLight g_PointLight[10];
    SpotLight g_SpotLight[10];
    Material g_Material;
    int g_NumDirLight;
    int g_NumPointLight;
    int g_NumSpotLight;
}

對應的C++結構體以下:

struct CBChangesEveryDrawing
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
};

struct CBChangesEveryFrame
{
    DirectX::XMMATRIX view;
    DirectX::XMFLOAT4 eyePos;
};

struct CBChangesOnResize
{
    DirectX::XMMATRIX proj;
};

struct CBChangesRarely
{
    DirectionalLight dirLight[10];
    PointLight pointLight[10];
    SpotLight spotLight[10];
    Material material;
    int numDirLight;
    int numPointLight;
    int numSpotLight;
    float pad;      // 打包保證16字節對齊
};

這裏主要更新頻率從快到慢分紅了四種:每次繪製物體時、每幀更新時、每次窗口大小變化時、從不更新。而後根據當前項目的實際需求將變量存放在合理的位置上。固然這樣子可能會致使不一樣着色器須要的變量放在了同一個塊上。不過着色器綁定常量緩衝區的操做能夠在一開始初始化的時候就完成,因此問題不大。

GameObject類--管理遊戲物體

因爲場景中的物體也在逐漸變多,爲了儘量方便地去管理每個物體,這裏實現了GameObject類:

class GameObject
{
public:
    GameObject();

    // 獲取位置
    DirectX::XMFLOAT3 GetPosition() const;
    // 設置緩衝區
    template<class VertexType, class IndexType>
    void SetBuffer(ID3D11Device * device, const Geometry::MeshData<VertexType, IndexType>& meshData);
    // 設置紋理
    void SetTexture(ID3D11ShaderResourceView * texture);
    // 設置矩陣
    void SetWorldMatrix(const DirectX::XMFLOAT4X4& world);
    void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX world);
    // 繪製
    void Draw(ID3D11DeviceContext * deviceContext);

    // 設置調試對象名
    // 若緩衝區被從新設置,調試對象名也須要被從新設置
    void SetDebugObjectName(const std::string& name);
private:
    DirectX::XMFLOAT4X4 m_WorldMatrix;                  // 世界矩陣
    ComPtr<ID3D11ShaderResourceView> m_pTexture;        // 紋理
    ComPtr<ID3D11Buffer> m_pVertexBuffer;               // 頂點緩衝區
    ComPtr<ID3D11Buffer> m_pIndexBuffer;                // 索引緩衝區
    UINT m_VertexStride;                                // 頂點字節大小
    UINT m_IndexCount;                                  // 索引數目 
};

然而目前的GameObject類還須要依賴GameApp類中的幾個常量緩衝區,到13章的時候就能夠獨立出來了。

須要額外注意的是,若是你想動態調整物體,能夠記錄物體的縮放、旋轉、平移量,而後在每一幀修改這些變量,最後再生成世界矩陣。

其中原來GameApp::InitResource方法中建立頂點和索引緩衝區的操做都轉移到了GameObject::SetBuffer上:

template<class VertexType, class IndexType>
void GameApp::GameObject::SetBuffer(ID3D11Device * device, const Geometry::MeshData<VertexType, IndexType>& meshData)
{
    // 釋放舊資源
    m_pVertexBuffer.Reset();
    m_pIndexBuffer.Reset();

    // 設置頂點緩衝區描述
    m_VertexStride = sizeof(VertexType);
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_IMMUTABLE;
    vbd.ByteWidth = (UINT)meshData.vertexVec.size() * m_VertexStride;
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = meshData.vertexVec.data();
    HR(device->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));


    // 設置索引緩衝區描述
    m_IndexCount = (UINT)meshData.indexVec.size();
    D3D11_BUFFER_DESC ibd;
    ZeroMemory(&ibd, sizeof(ibd));
    ibd.Usage = D3D11_USAGE_IMMUTABLE;
    ibd.ByteWidth = m_IndexCount * sizeof(IndexType);
    ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
    ibd.CPUAccessFlags = 0;
    // 新建索引緩衝區
    InitData.pSysMem = meshData.indexVec.data();
    HR(device->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));



}

ID3D11DeviceContext::XXGetConstantBuffers系列方法--獲取某一着色階段的常量緩衝區

這裏的XX能夠是VS, DS, CS, GS, HS, PS,即頂點着色階段、域着色階段、計算着色階段、幾何着色階段、外殼着色階段、像素着色階段。它們的形參基本上都是一致的,這裏只列舉ID3D11DeviceContext::VSGetConstantBuffers方法的形參含義:

void ID3D11DeviceContext::VSGetConstantBuffers( 
    UINT StartSlot,     // [In]指定的起始槽索引
    UINT NumBuffers,    // [In]常量緩衝區數目 
    ID3D11Buffer **ppConstantBuffers) = 0;    // [Out]常量固定緩衝區數組

最後GameObject::Draw方法以下,因爲內部已經承擔了轉置,所以在外部設置世界矩陣的時候不須要預先進行轉置

void GameApp::GameObject::Draw(ID3D11DeviceContext * deviceContext)
{
    // 設置頂點/索引緩衝區
    UINT strides = m_VertexStride;
    UINT offsets = 0;
    deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &strides, &offsets);
    deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    // 獲取以前已經綁定到渲染管線上的常量緩衝區並進行修改
    ComPtr<ID3D11Buffer> cBuffer = nullptr;
    deviceContext->VSGetConstantBuffers(0, 1, cBuffer.GetAddressOf());
    CBChangesEveryDrawing cbDrawing;

    // 內部進行轉置,這樣外部就不須要提早轉置了
    XMMATRIX W = XMLoadFloat4x4(&m_WorldMatrix);
    cbDrawing.world = XMMatrixTranspose(W);
    cbDrawing.worldInvTranspose = XMMatrixInverse(nullptr, W);  // 兩次轉置抵消

    // 更新常量緩衝區
    D3D11_MAPPED_SUBRESOURCE mappedData;
    HR(deviceContext->Map(cBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
    memcpy_s(mappedData.pData, sizeof(CBChangesEveryDrawing), &cbDrawing, sizeof(CBChangesEveryDrawing));
    deviceContext->Unmap(cBuffer.Get(), 0);

    // 設置紋理
    deviceContext->PSSetShaderResources(0, 1, m_pTexture.GetAddressOf());
    // 能夠開始繪製
    deviceContext->DrawIndexed(m_IndexCount, 0, 0);
}

這裏會對每次繪製須要更新的常量緩衝區進行修改

GameApp類的變化

GameApp::OnResize方法的變化

因爲攝像機保留有設置視錐體和視口的方法,而且須要更新常量緩衝區中的投影矩陣,所以該部分操做須要轉移到這裏進行:

void GameApp::OnResize()
{
    // 省略...
    D3DApp::OnResize();
    // 省略...
    
    // 攝像機變動顯示
    if (m_pCamera != nullptr)
    {
        m_pCamera->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
        m_pCamera->SetViewPort(0.0f, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
        m_CBOnResize.proj = XMMatrixTranspose(m_pCamera->GetProjXM());
        
        D3D11_MAPPED_SUBRESOURCE mappedData;
        HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[2].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
        memcpy_s(mappedData.pData, sizeof(CBChangesOnResize), &m_CBOnResize, sizeof(CBChangesOnResize));
        m_pd3dImmediateContext->Unmap(m_pConstantBuffers[2].Get(), 0);
    }
}

GameApp::InitResource方法的變化

該方法建立了牆體、地板和木箱三種遊戲物體,而後還建立了多個常量緩衝區,最後渲染管線的各個階段按須要綁定各類所需資源。這裏設置了一個平行光和一盞點光燈:

bool GameApp::InitResource()
{
    // ******************
    // 設置常量緩衝區描述
    D3D11_BUFFER_DESC cbd;
    ZeroMemory(&cbd, sizeof(cbd));
    cbd.Usage = D3D11_USAGE_DYNAMIC;
    cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    // 新建用於VS和PS的常量緩衝區
    cbd.ByteWidth = sizeof(CBChangesEveryDrawing);
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[0].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBChangesEveryFrame);
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[1].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBChangesOnResize);
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[2].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBChangesRarely);
    HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[3].GetAddressOf()));
    // ******************
    // 初始化遊戲對象
    ComPtr<ID3D11ShaderResourceView> texture;
    // 初始化木箱
    HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, texture.GetAddressOf()));
    m_WoodCrate.SetBuffer(m_pd3dDevice.Get(), Geometry::CreateBox());
    m_WoodCrate.SetTexture(texture.Get());
    
    // 初始化地板
    HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\floor.dds", nullptr, texture.ReleaseAndGetAddressOf()));
    m_Floor.SetBuffer(m_pd3dDevice.Get(),
        Geometry::CreatePlane(XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT2(20.0f, 20.0f), XMFLOAT2(5.0f, 5.0f)));
    m_Floor.SetTexture(texture.Get());

    // 初始化牆體
    m_Walls.resize(4);
    HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\brick.dds", nullptr, texture.ReleaseAndGetAddressOf()));
    // 這裏控制牆體四個面的生成
    for (int i = 0; i < 4; ++i)
    {
        m_Walls[i].SetBuffer(m_pd3dDevice.Get(),
            Geometry::CreatePlane(XMFLOAT3(), XMFLOAT2(20.0f, 8.0f), XMFLOAT2(5.0f, 1.5f)));
        XMMATRIX world = XMMatrixRotationX(-XM_PIDIV2) * XMMatrixRotationY(XM_PIDIV2 * i)
            * XMMatrixTranslation(i % 2 ? -10.0f * (i - 2) : 0.0f, 3.0f, i % 2 == 0 ? -10.0f * (i - 1) : 0.0f);
        m_Walls[i].SetWorldMatrix(world);
        m_Walls[i].SetTexture(texture.Get());
    }
        
    // 初始化採樣器狀態
    D3D11_SAMPLER_DESC sampDesc;
    ZeroMemory(&sampDesc, sizeof(sampDesc));
    sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
    sampDesc.MinLOD = 0;
    sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
    HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));

    
    // ******************
    // 初始化常量緩衝區的值
    // 初始化每幀可能會變化的值
    m_CameraMode = CameraMode::FirstPerson;
    auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
    m_pCamera = camera;
    camera->SetViewPort(0.0f, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
    camera->LookAt(XMFLOAT3(), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f));

    // 初始化僅在窗口大小變更時修改的值
    m_pCamera->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
    m_CBOnResize.proj = XMMatrixTranspose(m_pCamera->GetProjXM());

    // 初始化不會變化的值
    // 環境光
    m_CBRarely.dirLight[0].ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_CBRarely.dirLight[0].diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
    m_CBRarely.dirLight[0].specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_CBRarely.dirLight[0].direction = XMFLOAT3(0.0f, -1.0f, 0.0f);
    // 燈光
    m_CBRarely.pointLight[0].position = XMFLOAT3(0.0f, 10.0f, 0.0f);
    m_CBRarely.pointLight[0].ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_CBRarely.pointLight[0].diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
    m_CBRarely.pointLight[0].specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_CBRarely.pointLight[0].att = XMFLOAT3(0.0f, 0.1f, 0.0f);
    m_CBRarely.pointLight[0].range = 25.0f;
    m_CBRarely.numDirLight = 1;
    m_CBRarely.numPointLight = 1;
    m_CBRarely.numSpotLight = 0;
    // 初始化材質
    m_CBRarely.material.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    m_CBRarely.material.diffuse = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
    m_CBRarely.material.specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 50.0f);


    // 更新不容易被修改的常量緩衝區資源
    D3D11_MAPPED_SUBRESOURCE mappedData;
    HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[2].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
    memcpy_s(mappedData.pData, sizeof(CBChangesOnResize), &m_CBOnResize, sizeof(CBChangesOnResize));
    m_pd3dImmediateContext->Unmap(m_pConstantBuffers[2].Get(), 0);

    HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[3].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
    memcpy_s(mappedData.pData, sizeof(CBChangesRarely), &m_CBRarely, sizeof(CBChangesRarely));
    m_pd3dImmediateContext->Unmap(m_pConstantBuffers[3].Get(), 0);

    // ******************
    // 給渲染管線各個階段綁定好所需資源
    // 設置圖元類型,設定輸入佈局
    m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout3D.Get());
    // 默認綁定3D着色器
    m_pd3dImmediateContext->VSSetShader(m_pVertexShader3D.Get(), nullptr, 0);
    // 預先綁定各自所需的緩衝區,其中每幀更新的緩衝區須要綁定到兩個緩衝區上
    m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffers[0].GetAddressOf());
    m_pd3dImmediateContext->VSSetConstantBuffers(1, 1, m_pConstantBuffers[1].GetAddressOf());
    m_pd3dImmediateContext->VSSetConstantBuffers(2, 1, m_pConstantBuffers[2].GetAddressOf());

    m_pd3dImmediateContext->PSSetConstantBuffers(1, 1, m_pConstantBuffers[1].GetAddressOf());
    m_pd3dImmediateContext->PSSetConstantBuffers(3, 1, m_pConstantBuffers[3].GetAddressOf());
    m_pd3dImmediateContext->PSSetShader(m_pPixelShader3D.Get(), nullptr, 0);
    m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());

    return true;
}

GameApp::UpdateScene的變化

使用Mouse類的相對模式

在使用攝像機模式遊玩時,鼠標是不可見的。這時候能夠將鼠標模式設爲相對模式。

首先使用GetSystemMetrics函數來獲取當前屏幕分辨率,在CreateWindow的時候將窗口居中。

下面是D3DApp::InitMainWindow的變化:

bool D3DApp::InitMainWindow()
{
    // 省略不變部分...

    int screenWidth = GetSystemMetrics(SM_CXSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYSCREEN);

    // Compute window rectangle dimensions based on requested client area dimensions.
    RECT R = { 0, 0, m_ClientWidth, m_ClientHeight };
    AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
    int width = R.right - R.left;
    int height = R.bottom - R.top;

    m_hMainWnd = CreateWindow(L"D3DWndClassName", m_MainWndCaption.c_str(),
        WS_OVERLAPPEDWINDOW, (screenWidth - width) / 2, (screenHeight - height) / 2, width, height, 0, 0, m_hAppInst, 0);
  
    // 省略不變部分...

    return true;
}

而後GameApp::Init方法設置間接模式:

bool GameApp::Init()
{
    if (!D3DApp::Init())
        return false;

    if (!InitEffect())
        return false;

    if (!InitResource())
        return false;


    // 初始化鼠標,鍵盤不須要
    m_pMouse->SetWindow(m_hMainWnd);
    m_pMouse->SetMode(DirectX::Mouse::MODE_RELATIVE);
    return true;
}

最後就能夠開始獲取相對位移,並根據當前攝像機的模式和鍵鼠操做的狀態來進行對應操做:

void GameApp::UpdateScene(float dt)
{
    // 更新鼠標事件,獲取相對偏移量
    Mouse::State mouseState = m_pMouse->GetState();
    Mouse::State lastMouseState = m_MouseTracker.GetLastState();

    Keyboard::State keyState = m_pKeyboard->GetState();
    m_KeyboardTracker.Update(keyState);

    // 獲取子類
    auto cam1st = std::dynamic_pointer_cast<FirstPersonCamera>(m_pCamera);
    auto cam3rd = std::dynamic_pointer_cast<ThirdPersonCamera>(m_pCamera);

    
    if (m_CameraMode == CameraMode::FirstPerson || m_CameraMode == CameraMode::Free)
    {
        // 第一人稱/自由攝像機的操做

        // 方向移動
        if (keyState.IsKeyDown(Keyboard::W))
        {
            if (m_CameraMode == CameraMode::FirstPerson)
                cam1st->Walk(dt * 3.0f);
            else
                cam1st->MoveForward(dt * 3.0f);
        }   
        if (keyState.IsKeyDown(Keyboard::S))
        {
            if (m_CameraMode == CameraMode::FirstPerson)
                cam1st->Walk(dt * -3.0f);
            else
                cam1st->MoveForward(dt * -3.0f);
        }
        if (keyState.IsKeyDown(Keyboard::A))
            cam1st->Strafe(dt * -3.0f);
        if (keyState.IsKeyDown(Keyboard::D))
            cam1st->Strafe(dt * 3.0f);

        // 將位置限制在[-8.9f, 8.9f]的區域內
        // 不容許穿地
        XMFLOAT3 adjustedPos;
        XMStoreFloat3(&adjustedPos, XMVectorClamp(cam1st->GetPositionXM(), XMVectorSet(-8.9f, 0.0f, -8.9f, 0.0f), XMVectorReplicate(8.9f)));
        cam1st->SetPosition(adjustedPos);

        // 僅在第一人稱模式移動箱子
        if (m_CameraMode == CameraMode::FirstPerson)
            m_WoodCrate.SetWorldMatrix(XMMatrixTranslation(adjustedPos.x, adjustedPos.y, adjustedPos.z));
        // 視野旋轉,防止開始的差值過大致使的忽然旋轉
        cam1st->Pitch(mouseState.y * dt * 1.25f);
        cam1st->RotateY(mouseState.x * dt * 1.25f);
    }
    else if (m_CameraMode == CameraMode::ThirdPerson)
    {
        // 第三人稱攝像機的操做

        cam3rd->SetTarget(m_WoodCrate.GetPosition());

        // 繞物體旋轉
        cam3rd->RotateX(mouseState.y * dt * 1.25f);
        cam3rd->RotateY(mouseState.x * dt * 1.25f);
        cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);
    }

    // 更新觀察矩陣
    m_pCamera->UpdateViewMatrix();
    XMStoreFloat4(&m_CBFrame.eyePos, m_pCamera->GetPositionXM());
    m_CBFrame.view = XMMatrixTranspose(m_pCamera->GetViewXM());

    // 重置滾輪值
    m_pMouse->ResetScrollWheelValue();
    
    // 攝像機模式切換
    if (m_KeyboardTracker.IsKeyPressed(Keyboard::D1) && m_CameraMode != CameraMode::FirstPerson)
    {
        if (!cam1st)
        {
            cam1st.reset(new FirstPersonCamera);
            cam1st->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
            m_pCamera = cam1st;
        }

        cam1st->LookTo(m_WoodCrate.GetPosition(),
            XMFLOAT3(0.0f, 0.0f, 1.0f),
            XMFLOAT3(0.0f, 1.0f, 0.0f));
        
        m_CameraMode = CameraMode::FirstPerson;
    }
    else if (m_KeyboardTracker.IsKeyPressed(Keyboard::D2) && m_CameraMode != CameraMode::ThirdPerson)
    {
        if (!cam3rd)
        {
            cam3rd.reset(new ThirdPersonCamera);
            cam3rd->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
            m_pCamera = cam3rd;
        }
        XMFLOAT3 target = m_WoodCrate.GetPosition();
        cam3rd->SetTarget(target);
        cam3rd->SetDistance(8.0f);
        cam3rd->SetDistanceMinMax(3.0f, 20.0f);
        
        m_CameraMode = CameraMode::ThirdPerson;
    }
    else if (m_KeyboardTracker.IsKeyPressed(Keyboard::D3) && m_CameraMode != CameraMode::Free)
    {
        if (!cam1st)
        {
            cam1st.reset(new FirstPersonCamera);
            cam1st->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
            m_pCamera = cam1st;
        }
        // 從箱子上方開始
        XMFLOAT3 pos = m_WoodCrate.GetPosition();
        XMFLOAT3 to = XMFLOAT3(0.0f, 0.0f, 1.0f);
        XMFLOAT3 up = XMFLOAT3(0.0f, 1.0f, 0.0f);
        pos.y += 3;
        cam1st->LookTo(pos, to, up);

        m_CameraMode = CameraMode::Free;
    }
    // 退出程序,這裏應向窗口發送銷燬信息
    if (keyState.IsKeyDown(Keyboard::Escape))
        SendMessage(MainWnd(), WM_DESTROY, 0, 0);
    
    D3D11_MAPPED_SUBRESOURCE mappedData;
    HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
    memcpy_s(mappedData.pData, sizeof(CBChangesEveryFrame), &m_CBFrame, sizeof(CBChangesEveryFrame));
    m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);
}

其中對攝像機位置使用XMVectorClamp函數是爲了將X, Y和Z值都限制在範圍爲[-8.9, 8.9]的立方體活動區域防止跑出場景區域外,但使用第三人稱攝像機的時候沒有這樣的限制,由於能夠營造出一種透視觀察的效果。

GameApp::DrawScene的變化

該方法變化不大,具體以下:

void GameApp::DrawScene()
{
    assert(m_pd3dImmediateContext);
    assert(m_pSwapChain);

    m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
    m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    //
    // 繪製幾何模型
    //
    m_WoodCrate.Draw(m_pd3dImmediateContext.Get());
    m_Floor.Draw(m_pd3dImmediateContext.Get());
    for (auto& wall : m_Walls)
        wall.Draw(m_pd3dImmediateContext.Get());

    //
    // 繪製Direct2D部分
    //
    
    // ...

    HR(m_pSwapChain->Present(0, 0));
}

最後下面演示了三種模式下的操做效果:

練習題

  1. 在第三人稱模式下,讓物體也可以進行先後、左右的平移運動
  2. 在第三人稱模式下,使用平躺的圓柱體,讓其左右平移運動改成左右旋轉運動,先後運動改成朝前滾動
  3. 在第一人稱模式下,給攝像機添加對"滾動"的支持(繞Look軸旋轉)

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索