DirectX11 With Windows SDK--10 基於Transform的攝像機類與GameObject類


前言

注意:本教程僅針對代碼版本1.27.2及更高版本的項目,仍在使用舊版本代碼的用戶請留意更新項目代碼

在本教程中,以前的第一人稱攝像機實現是源自龍書的做法。考慮到攝像機的觀察矩陣和物體的世界矩陣實質上是有一定的聯系,因此可以將這部分變換給剝離出來,使用統一的Transform類,然后攝像機類與游戲對象類都使用Transform類來分別完成觀察變換、世界變換。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

Transform類

世界矩陣和觀察矩陣的聯系

若已知物體所在位置\(\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})\),並且物體的xyz縮放比例都為1,則我們可以得到對應的世界矩陣:

\[\mathbf{W}=\mathbf{RT}= \begin{bmatrix} u_{x} & u_{y} & u_{z} & 0 \\ v_{x} & v_{y} & v_{z} & 0 \\ w_{x} & w_{y} & w_{z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ Q_{x} & Q_{y} & Q_{z} & 1 \end{bmatrix}= \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} \]

我們可以把上述變換看作:將物體從世界坐標原點搬移到世界坐標系對應的位置,並按其坐標軸做對應朝向和大小的調整。

然而現在我們需要做的是從世界坐標系轉換到觀察空間坐標系,如果把攝像機看做物體的話,實際上觀察空間坐標系就是攝像機物體的局部坐標系(右方向為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} \]

世界變換的復合

假設在初始化階段,物體經歷了初始旋轉變換和平移變換,則此時的變換矩陣為

\[\mathbf{W_{0}=R_{0} T_{0}} \]

若每一幀都產生縮放、旋轉和位移,把當前幀的縮放、旋轉和位移矩陣分別記為\(S_{n}\)\(R_{n}\)\(T_{n}\),有的人會認為當前幀的世界變換為:

\[\mathbf{W_{n}=W_{n-1}S_{n}R_{n}T_{n}} \]

然而這種復合變換可能會導致異常的變換,在前面的變換一章我們就舉了一個例子。歸根結底在於矩陣乘法不滿足交換律,且縮放操作如果不先進行(前面還有旋轉或平移)的話會導致物體產生畸變。所以要先讓縮放進行:

\[\mathbf{W_{n}=S_{0}S_{1}...S_{n}...} \]

此時旋轉和平移操作就可以自行指定順序了。如果先平移再旋轉,離開原點的物體會繞原點旋轉。如果先旋轉再平移,就是先改變物體的朝向再放到指定的位置上。

通常對物體修改旋轉是基於原點位置的旋轉,因此它的世界變換復合為:

\[\mathbf{W_{n}=S_{0}S_{1}...S_{n}R_{0}R_{1}...R_{n}T_{0}T_{1}...T_{n}} \]

而如果涉及到具有父子關系物體的世界變換,則會更復雜一些。以Unity的為例,設父級物體的縮放矩陣、旋轉矩陣和位移矩陣分別為\(S_{0}\)\(R_{0}\)\(T_{0}\),子一級物體的縮放矩陣、旋轉矩陣和位移矩陣為\(S_{1}\)\(R_{1}\)\(T_{1}\)。。。那么子N級物體的世界變換復合為:

\[\mathbf{W_{n}=S_{0}S_{1}...S_{n}R_{n}T_{n}R_{n-1}T_{n-1}...R_{0}T_{0}} \]

變換的分解更新

根據上面的復合形式,我們可以將縮放、旋轉和平移部分拆開來分別更新,其中縮放和平移部分都可以使用一個向量來保存,然后縮放矩陣的連乘相當於縮放向量的分量乘法,平移矩陣的連乘相當於平移向量的分量加法。至於旋轉矩陣的表示形式有三種:旋轉四元數、基於特定的坐標軸順序進行旋轉的三個旋轉歐拉角、三個相互正交的坐標軸向量。這三種表示形式是可以相互轉換的。由於到目前旋轉四元數在教程中還沒涉及,因此為了節省存儲空間,我們使用旋轉歐拉角的形式保存。等學到四元數之后就可以用它替換歐拉角存儲了。

Transform類的定義如下:

class Transform
{
public:
    Transform() = default;
    Transform(const DirectX::XMFLOAT3& scale, const DirectX::XMFLOAT3& rotation, const DirectX::XMFLOAT3& position);
    ~Transform() = default;

    Transform(const Transform&) = default;
    Transform& operator=(const Transform&) = default;

    Transform(Transform&&) = default;
    Transform& operator=(Transform&&) = default;

    // 獲取對象縮放比例
    DirectX::XMFLOAT3 GetScale() const;
    // 獲取對象縮放比例
    DirectX::XMVECTOR GetScaleXM() const;

    // 獲取對象歐拉角(弧度制)
    // 對象以Z-X-Y軸順序旋轉
    DirectX::XMFLOAT3 GetRotation() const;
    // 獲取對象歐拉角(弧度制)
    // 對象以Z-X-Y軸順序旋轉
    DirectX::XMVECTOR GetRotationXM() const;

    // 獲取對象位置
    DirectX::XMFLOAT3 GetPosition() const;
    // 獲取對象位置
    DirectX::XMVECTOR GetPositionXM() const;

    // 獲取右方向軸
    DirectX::XMFLOAT3 GetRightAxis() const;
    // 獲取右方向軸
    DirectX::XMVECTOR GetRightAxisXM() const;

    // 獲取上方向軸
    DirectX::XMFLOAT3 GetUpAxis() const;
    // 獲取上方向軸
    DirectX::XMVECTOR GetUpAxisXM() const;

    // 獲取前方向軸
    DirectX::XMFLOAT3 GetForwardAxis() const;
    // 獲取前方向軸
    DirectX::XMVECTOR GetForwardAxisXM() const;

    // 獲取世界變換矩陣
    DirectX::XMFLOAT4X4 GetLocalToWorldMatrix() const;
    // 獲取世界變換矩陣
    DirectX::XMMATRIX GetLocalToWorldMatrixXM() const;

    // 獲取逆世界變換矩陣
    DirectX::XMFLOAT4X4 GetWorldToLocalMatrix() const;
    // 獲取逆世界變換矩陣
    DirectX::XMMATRIX GetWorldToLocalMatrixXM() const;

    // 設置對象縮放比例
    void SetScale(const DirectX::XMFLOAT3& scale);
    // 設置對象縮放比例
    void SetScale(float x, float y, float z);

    // 設置對象歐拉角(弧度制)
    // 對象將以Z-X-Y軸順序旋轉
    void SetRotation(const DirectX::XMFLOAT3& eulerAnglesInRadian);
    // 設置對象歐拉角(弧度制)
    // 對象將以Z-X-Y軸順序旋轉
    void SetRotation(float x, float y, float z);

    // 設置對象位置
    void SetPosition(const DirectX::XMFLOAT3& position);
    // 設置對象位置
    void SetPosition(float x, float y, float z);
    
    // 指定歐拉角旋轉對象
    void Rotate(const DirectX::XMFLOAT3& eulerAnglesInRadian);
    // 指定以原點為中心繞軸旋轉
    void RotateAxis(const DirectX::XMFLOAT3& axis, float radian);
    // 指定以point為旋轉中心繞軸旋轉
    void RotateAround(const DirectX::XMFLOAT3& point, const DirectX::XMFLOAT3& axis, float radian);

    // 沿着某一方向平移
    void Translate(const DirectX::XMFLOAT3& direction, float magnitude);

    // 觀察某一點
    void LookAt(const DirectX::XMFLOAT3& target, const DirectX::XMFLOAT3& up = { 0.0f, 1.0f, 0.0f });
    // 沿着某一方向觀察
    void LookTo(const DirectX::XMFLOAT3& direction, const DirectX::XMFLOAT3& up = { 0.0f, 1.0f, 0.0f });

private:
    // 從旋轉矩陣獲取旋轉歐拉角
    DirectX::XMFLOAT3 GetEulerAnglesFromRotationMatrix(const DirectX::XMFLOAT4X4& rotationMatrix);

private:
    DirectX::XMFLOAT3 m_Scale = { 1.0f, 1.0f, 1.0f };                // 縮放
    DirectX::XMFLOAT3 m_Rotation = {};                                // 旋轉歐拉角(弧度制)
    DirectX::XMFLOAT3 m_Position = {};                                // 位置
};

部分實現如下:

XMFLOAT3 Transform::GetScale() const
{
    return m_Scale;
}

XMFLOAT3 Transform::GetRotation() const
{
    return m_Rotation;
}

XMFLOAT3 Transform::GetPosition() const
{
    return m_Position;
}

XMFLOAT3 Transform::GetRightAxis() const
{
    XMMATRIX R = XMMatrixRotationRollPitchYawFromVector(XMLoadFloat3(&m_Rotation));
    XMFLOAT3 right;
    XMStoreFloat3(&right, R.r[0]);
    return right;
}

XMFLOAT3 Transform::GetUpAxis() const
{
    XMMATRIX R = XMMatrixRotationRollPitchYawFromVector(XMLoadFloat3(&m_Rotation));
    XMFLOAT3 up;
    XMStoreFloat3(&up, R.r[1]);
    return up;
}

XMFLOAT3 Transform::GetForwardAxis() const
{
    XMMATRIX R = XMMatrixRotationRollPitchYawFromVector(XMLoadFloat3(&m_Rotation));
    XMFLOAT3 forward;
    XMStoreFloat3(&forward, R.r[2]);
    return forward;
}

void Transform::SetScale(const XMFLOAT3& scale)
{
    m_Scale = scale;
}

void Transform::SetRotation(const XMFLOAT3& eulerAnglesInRadian)
{
    m_Rotation = eulerAnglesInRadian;
}

void Transform::SetPosition(const XMFLOAT3& position)
{
    m_Position = position;
}

void Transform::Translate(const XMFLOAT3& direction, float magnitude)
{
    XMVECTOR directionVec = XMVector3Normalize(XMLoadFloat3(&direction));
    XMVECTOR newPosition = XMVectorMultiplyAdd(XMVectorReplicate(magnitude), directionVec, XMLoadFloat3(&m_Position));
    XMStoreFloat3(&m_Position, newPosition);
}

void Transform::LookAt(const XMFLOAT3& target, const XMFLOAT3& up)
{
    XMMATRIX View = XMMatrixLookAtLH(XMLoadFloat3(&m_Position), XMLoadFloat3(&target), XMLoadFloat3(&up));
    XMMATRIX InvView = XMMatrixInverse(nullptr, View);
    XMFLOAT4X4 rotMatrix;
    XMStoreFloat4x4(&rotMatrix, InvView);
    m_Rotation = GetEulerAnglesFromRotationMatrix(rotMatrix);
}

void Transform::LookTo(const XMFLOAT3& direction, const XMFLOAT3& up)
{
    XMMATRIX View = XMMatrixLookToLH(XMLoadFloat3(&m_Position), XMLoadFloat3(&direction), XMLoadFloat3(&up));
    XMMATRIX InvView = XMMatrixInverse(nullptr, View);
    XMFLOAT4X4 rotMatrix;
    XMStoreFloat4x4(&rotMatrix, InvView);
    m_Rotation = GetEulerAnglesFromRotationMatrix(rotMatrix);
}

規定旋轉順序

在決定X軸、Y軸、Z軸旋轉的先后順序時,可以產生出6種不同的組合。而在DirectXMath中,存在這樣一系列旋轉矩陣構造函數,采用的是Roll-Pitch-Yaw的旋轉形式,實際上說的就是先繞Z軸旋轉,再繞X軸旋轉,最后再繞Y軸旋轉。選擇這樣的旋轉順序,首先考慮到的是要最大限度避免萬向節死鎖的出現(簡單來說,就是在進行第二步旋轉時旋轉了+-90度,會導致第一步旋轉和第三步旋轉看起來是繞方向相反的兩個軸進行旋轉,丟失了一個旋轉自由度。具體不展開),而物體+Z軸豎直朝上或朝下的情況都是非常少見的。

旋轉歐拉角與旋轉矩陣的相互轉化

在Z-X-Y的旋轉順序下,由旋轉歐拉角產生的旋轉矩陣為:

\[\begin{align} \mathbf{R_z(\theta_{z})R_x(\theta_{x})R_y(\theta_{y})} &= \begin{bmatrix} cos\theta_{z} & sin\theta_{z} & 0 & 0 \\ -sin\theta_{z} & cos\theta_{z} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & cos\theta_{x} & sin\theta_{x} & 0 \\ 0 & -sin\theta_{x} & cos\theta_{x} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} cos\theta_{y} & 0 & -sin\theta_{y} & 0 \\ 0 & 1 & 0 & 0 \\ sin\theta_{y} & 0 & cos\theta_{y} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\ &= \begin{bmatrix} -sin\theta_{z}sin\theta_{x}sin\theta_{y}+cos\theta_{z}cos\theta_{y} & -sin\theta_{z}cos\theta_{x} & sin\theta_{z}sin\theta_{x}cos\theta_{y} + cos\theta_{z}sin\theta_{y} & 0 \\ -cos\theta_{z}sin\theta_{x}sin\theta_{y}+sin\theta_{z}cos\theta_{y} & cos\theta_{z}cos\theta_{x} & -cos\theta_{z}sin\theta_{x}cos\theta_{y} + sin\theta_{z}sin\theta_{y} & 0 \\ -cos\theta_{x}sin\theta_{y} & sin\theta_{x} & cos\theta_{x}cos\theta_{y} & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{align} \]

在DirectXMath中我們可以調用XMMatrixRotationRollPitchYawXMMatrixRotationRollPitchYawFromVector函數來獲取變換矩陣。

通過該矩陣,我們可以還原出歐拉角。規定\(m_{11}\)為矩陣第一行第一列元素,則有:

\[\theta_{x}=atan2(m_{32}, \sqrt{1-m_{32}^{2}}) \\ \theta_{y}=atan2(-m_{31}, m_{33}) \\ \theta_{z}=atan2(-m_{12}, m_{22}) \]

但在還原歐拉角的時候,由於浮點數的精度問題,可能會導致\(m_32\)莫名其妙地大於1,從而導致根式部分無定義。

因此我們的歐拉角還原函數如下:

XMFLOAT3 Transform::GetEulerAnglesFromRotationMatrix(const XMFLOAT4X4& rotationMatrix)
{
    // 通過旋轉矩陣反求歐拉角
    float c = sqrtf(1.0f - rotationMatrix(2, 1) * rotationMatrix(2, 1));
    // 防止r[2][1]出現大於1的情況
    if (isnan(c))
        c = 0.0f;
    XMFLOAT3 rotation;
    rotation.z = atan2f(rotationMatrix(0, 1), rotationMatrix(1, 1));
    rotation.x = atan2f(-rotationMatrix(2, 1), c);
    rotation.y = atan2f(rotationMatrix(2, 0), rotationMatrix(2, 2));
    return rotation;
}

旋轉相關的函數

首先最簡單的就是基於旋轉歐拉角的旋轉了,只需要更新歐拉角即可:

void Transform::Rotate(const XMFLOAT3& eulerAnglesInRadian)
{
    XMVECTOR newRotationVec = XMVectorAdd(XMLoadFloat3(&m_Rotation), 
        XMLoadFloat3(&eulerAnglesInRadian));
    XMStoreFloat3(&m_Rotation, newRotationVec);
}

接着是繞軸旋轉,先根據當前歐拉角得到旋轉矩陣,然后更新,最后還原歐拉角:

void Transform::RotateAxis(const XMFLOAT3& axis, float radian)
{
    XMVECTOR rotationVec = XMLoadFloat3(&m_Rotation);
    XMMATRIX R = XMMatrixRotationRollPitchYawFromVector(rotationVec) * 
        XMMatrixRotationAxis(XMLoadFloat3(&axis), radian);
    XMFLOAT4X4 rotMatrix;
    XMStoreFloat4x4(&rotMatrix, R);
    m_Rotation = GetEulerAnglesFromRotationMatrix(rotMatrix);
}

基於某一點為旋轉中心進行繞軸旋轉的實現過程稍微有點復雜。首先根據已有變換算出旋轉矩陣*平移矩陣,然后將旋轉中心平移到原點(這兩步平移可以合並),再進行旋轉,最后再平移回旋轉中心:

void Transform::RotateAround(const XMFLOAT3& point, const XMFLOAT3& axis, float radian)
{
	XMVECTOR rotationVec = XMLoadFloat3(&m_Rotation);
	XMVECTOR positionVec = XMLoadFloat3(&m_Position);
	XMVECTOR centerVec = XMLoadFloat3(&point);

	// 以point作為原點進行旋轉
	XMMATRIX RT = XMMatrixRotationRollPitchYawFromVector(rotationVec) * XMMatrixTranslationFromVector(positionVec - centerVec);
	RT *= XMMatrixRotationAxis(XMLoadFloat3(&axis), radian);
	RT *= XMMatrixTranslationFromVector(centerVec);
	XMFLOAT4X4 rotMatrix;
	XMStoreFloat4x4(&rotMatrix, RT);
	m_Rotation = GetEulerAnglesFromRotationMatrix(rotMatrix);
	XMStoreFloat3(&m_Position, RT.r[3]);
}

構造變換矩陣

完成更新后,我們就可以獲取變換矩陣了。

其中GetLocalToWorldMatrix系列函數用於從局部坐標系變換到世界坐標系,而GetWorldToLocalMatrix系列函數用於從世界坐標系變換到局部坐標系,當Scale=(1,1,1)時,它可以表示為觀察矩陣。

XMFLOAT4X4 Transform::GetLocalToWorldMatrix() const
{
    XMFLOAT4X4 res;
    XMStoreFloat4x4(&res, GetLocalToWorldMatrixXM());
    return res;
}

XMMATRIX Transform::GetLocalToWorldMatrixXM() const
{
    XMVECTOR scaleVec = XMLoadFloat3(&m_Scale);
    XMVECTOR rotationVec = XMLoadFloat3(&m_Rotation);
    XMVECTOR positionVec = XMLoadFloat3(&m_Position);
    XMMATRIX World = XMMatrixScalingFromVector(scaleVec) * XMMatrixRotationRollPitchYawFromVector(rotationVec) * XMMatrixTranslationFromVector(positionVec);
    return World;
}

XMFLOAT4X4 Transform::GetWorldToLocalMatrix() const
{
    XMFLOAT4X4 res;
    XMStoreFloat4x4(&res, GetWorldToLocalMatrixXM());
    return res;
}

XMMATRIX Transform::GetWorldToLocalMatrixXM() const
{
    XMMATRIX InvWorld = XMMatrixInverse(nullptr, GetLocalToWorldMatrixXM());
    return InvWorld;
}

Camera類

攝像機抽象基類

現在的攝像機將基於Transform實現。Camera類的定義如下(刪去了一些用不上的函數):

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

    //
    // 獲取攝像機位置
    //

    DirectX::XMVECTOR GetPositionXM() const;
    DirectX::XMFLOAT3 GetPosition() const;

    //
    // 獲取攝像機旋轉
    //

    // 獲取繞X軸旋轉的歐拉角弧度
    float GetRotationX() const;
    // 獲取繞Y軸旋轉的歐拉角弧度
    float GetRotationY() const;

    //
    // 獲取攝像機的坐標軸向量
    //

    DirectX::XMVECTOR GetRightAxisXM() const;
    DirectX::XMFLOAT3 GetRightAxis() const;
    DirectX::XMVECTOR GetUpAxisXM() const;
    DirectX::XMFLOAT3 GetUpAxis() const;
    DirectX::XMVECTOR GetLookAxisXM() const;
    DirectX::XMFLOAT3 GetLookAxis() 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);

protected:

    // 攝像機的變換
    Transform m_Transform = {};
    
    // 視錐體屬性
    float m_NearZ = 0.0f;
    float m_FarZ = 0.0f;
    float m_Aspect = 0.0f;
    float m_FovY = 0.0f;

    // 當前視口
    D3D11_VIEWPORT m_ViewPort = {};

};

可以看到,無論是什么類型的攝像機,都一定需要包含觀察矩陣、投影矩陣以及設置這兩個坐標系所需要的一些相關信息。

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

FirstPersonCamera類的定義如下:

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

    // 設置攝像機位置
    void SetPosition(float x, float y, float z);
    void SetPosition(const DirectX::XMFLOAT3& pos);
    // 設置攝像機的朝向
    void LookAt(const DirectX::XMFLOAT3& pos, const DirectX::XMFLOAT3& target,const DirectX::XMFLOAT3& 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);
    // 上下觀察
    // 正rad值向上觀察
    // 負rad值向下觀察
    void Pitch(float rad);
    // 左右觀察
    // 正rad值向左觀察
    // 負rad值向右觀察
    void RotateY(float rad);
};

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

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

具體實現如下:

FirstPersonCamera::~FirstPersonCamera()
{
}

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

void FirstPersonCamera::SetPosition(const XMFLOAT3& pos)
{
    m_Transform.SetPosition(pos);
}

void FirstPersonCamera::LookAt(const XMFLOAT3 & pos, const XMFLOAT3 & target,const XMFLOAT3 & up)
{
    m_Transform.SetPosition(pos);
    m_Transform.LookAt(target, up);
}

void FirstPersonCamera::LookTo(const XMFLOAT3 & pos, const XMFLOAT3 & to, const XMFLOAT3 & up)
{
    m_Transform.SetPosition(pos);
    m_Transform.LookTo(to, up);
}

void FirstPersonCamera::Strafe(float d)
{
    m_Transform.Translate(m_Transform.GetRightAxis(), d);
}

void FirstPersonCamera::Walk(float d)
{
    XMVECTOR rightVec = XMLoadFloat3(&m_Transform.GetRightAxis());
    XMVECTOR frontVec = XMVector3Normalize(XMVector3Cross(rightVec, g_XMIdentityR1));
    XMFLOAT3 front;
    XMStoreFloat3(&front, frontVec);
    m_Transform.Translate(front, d);
}

void FirstPersonCamera::MoveForward(float d)
{
    m_Transform.Translate(m_Transform.GetForwardAxis(), d);
}

void FirstPersonCamera::Pitch(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    // 將繞x軸旋轉弧度限制在[-7pi/18, 7pi/18]之間
    rotation.x += rad;
    if (rotation.x > XM_PI * 7 / 18)
        rotation.x = XM_PI * 7 / 18;
    else if (rotation.x < -XM_PI * 7 / 18)
        rotation.x = -XM_PI * 7 / 18;

    m_Transform.SetRotation(rotation);
}

void FirstPersonCamera::RotateY(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    rotation.y = XMScalarModAngle(rotation.y + rad);
    m_Transform.SetRotation(rotation);
}

其中上下視野角度由旋轉歐拉角x分量決定,將其限制在+-70°的范圍內避免過度抬頭和低頭。

第三人稱攝像機

ThirdPersonCamera類的定義如下:

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

    // 獲取當前跟蹤物體的位置
    DirectX::XMFLOAT3 GetTargetPosition() const;
    // 獲取與物體的距離
    float GetDistance() const;
    // 繞物體垂直旋轉(注意繞x軸旋轉歐拉角弧度限制在[0, pi/3])
    void RotateX(float rad);
    // 繞物體水平旋轉
    void RotateY(float rad);
    // 拉近物體
    void Approach(float dist);
    // 設置初始繞X軸的弧度(注意繞x軸旋轉歐拉角弧度限制在[0, pi/3])
    void SetRotationX(float rad);
    // 設置初始繞Y軸的弧度
    void SetRotationY(float rad);
    // 設置並綁定待跟蹤物體的位置
    void SetTarget(const DirectX::XMFLOAT3& target);
    // 設置初始距離
    void SetDistance(float dist);
    // 設置最小最大允許距離
    void SetDistanceMinMax(float minDist, float maxDist);

private:
    DirectX::XMFLOAT3 m_Target = {};
    float m_Distance = 0.0f;
    // 最小允許距離,最大允許距離
    float m_MinDist = 0.0f, m_MaxDist = 0.0f;
};

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

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

上述部分具體實現如下:

XMFLOAT3 ThirdPersonCamera::GetTargetPosition() const
{
    return m_Target;
}

float ThirdPersonCamera::GetDistance() const
{
    return m_Distance;
}

void ThirdPersonCamera::RotateX(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    // 將繞x軸旋轉弧度限制在[0, pi/3]之間
    rotation.x += rad;
    if (rotation.x < 0.0f)
        rotation.x = 0.0f;
    else if (rotation.x > XM_PI / 3)
        rotation.x = XM_PI / 3;

    m_Transform.SetRotation(rotation);
    m_Transform.SetPosition(m_Target);
    m_Transform.Translate(m_Transform.GetForwardAxis(), -m_Distance);
}

void ThirdPersonCamera::RotateY(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    rotation.y = XMScalarModAngle(rotation.y + rad);

    m_Transform.SetRotation(rotation);
    m_Transform.SetPosition(m_Target);
    m_Transform.Translate(m_Transform.GetForwardAxis(), -m_Distance);
}

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;

    m_Transform.SetPosition(m_Target);
    m_Transform.Translate(m_Transform.GetForwardAxis(), -m_Distance);
}

void ThirdPersonCamera::SetRotationX(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    // 將繞x軸旋轉弧度限制在[-pi/3, 0]之間
    rotation.x = rad;
    if (rotation.x > 0.0f)
        rotation.x = 0.0f;
    else if (rotation.x < -XM_PI / 3)
        rotation.x = -XM_PI / 3;

    m_Transform.SetRotation(rotation);
    m_Transform.SetPosition(m_Target);
    m_Transform.Translate(m_Transform.GetForwardAxis(), -m_Distance);
}

void ThirdPersonCamera::SetRotationY(float rad)
{
    XMFLOAT3 rotation = m_Transform.GetRotation();
    rotation.y = XMScalarModAngle(rad);
    m_Transform.SetRotation(rotation);
    m_Transform.SetPosition(m_Target);
    m_Transform.Translate(m_Transform.GetForwardAxis(), -m_Distance);
}

void ThirdPersonCamera::SetTarget(const 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;
}


這里唯一要討論的討論就是如何根據旋轉歐拉角x和y分量、距離dist信息來構造出攝像機的最終位置和朝向。實際上就是先利用歐拉角進行旋轉變換,得到攝像機局部坐標系的三個坐標軸朝向,然后利用Look方向軸向后移動dist個單位得到最終位置。

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

由於項目正在逐漸變得更加龐大,常量緩沖區會頻繁更新,但是每次更新常量緩沖區都必須將整個塊的內容都刷新一遍,如果只是為了更新里面其中一個變量就要進行一次塊的刷新,這樣會導致性能上的損耗。所以將常量緩沖區根據刷新頻率和類別來進行更細致的分塊,盡可能(但不一定能完全做到)保證每一次更新都不會有變量在進行無意義的刷新。因此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:
    Transform m_Transform                               // 世界矩陣
    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_R32_UINT, 0);

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

    // 內部進行轉置
    XMMATRIX W = m_Transform.GetLocalToWorldMatrixXM();
    cbDrawing.world = XMMatrixTranspose(W);
    cbDrawing.worldInvTranspose = XMMatrixTranspose(InverseTranspose(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(XMFLOAT2(20.0f, 20.0f), XMFLOAT2(5.0f, 5.0f)));
    m_Floor.SetTexture(texture.Get());
    m_Floor.GetTransform().SetPosition(0.0f, -1.0f, 0.0f);
    
    
    // 初始化牆體
    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(XMFLOAT2(20.0f, 8.0f), XMFLOAT2(5.0f, 1.5f)));
        Transform& transform = m_Walls[i].GetTransform();
        transform.SetRotation(-XM_PIDIV2, XM_PIDIV2 * i, 0.0f);
        transform.SetPosition(i % 2 ? -10.0f * (i - 2) : 0.0f, 3.0f, i % 2 == 0 ? -10.0f * (i - 1) : 0.0f);
        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());

    // ******************
    // 設置調試對象名
    //
    D3D11SetDebugObjectName(m_pVertexLayout2D.Get(), "VertexPosTexLayout");
    D3D11SetDebugObjectName(m_pVertexLayout3D.Get(), "VertexPosNormalTexLayout");
    D3D11SetDebugObjectName(m_pConstantBuffers[0].Get(), "CBDrawing");
    D3D11SetDebugObjectName(m_pConstantBuffers[1].Get(), "CBFrame");
    D3D11SetDebugObjectName(m_pConstantBuffers[2].Get(), "CBOnResize");
    D3D11SetDebugObjectName(m_pConstantBuffers[3].Get(), "CBRarely");
    D3D11SetDebugObjectName(m_pVertexShader2D.Get(), "Basic_VS_2D");
    D3D11SetDebugObjectName(m_pVertexShader3D.Get(), "Basic_VS_3D");
    D3D11SetDebugObjectName(m_pPixelShader2D.Get(), "Basic_PS_2D");
    D3D11SetDebugObjectName(m_pPixelShader3D.Get(), "Basic_PS_3D");
    D3D11SetDebugObjectName(m_pSamplerState.Get(), "SSLinearWrap");
    m_Floor.SetDebugObjectName("Floor");
    m_WoodCrate.SetDebugObjectName("WoodCrate");
    m_Walls[0].SetDebugObjectName("Walls[0]");
    m_Walls[1].SetDebugObjectName("Walls[1]");
    m_Walls[2].SetDebugObjectName("Walls[2]");
    m_Walls[3].SetDebugObjectName("Walls[3]");


    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);

    Transform& woodCrateTransform = m_WoodCrate.GetTransform();

    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.9, 8.9]x[-8.9, 8.9]x[0.0, 8.9]的區域內
        // 不允許穿地
        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)
            woodCrateTransform.SetPosition(adjustedPos);
        // 在鼠標沒進入窗口前仍為ABSOLUTE模式
        if (mouseState.positionMode == Mouse::MODE_RELATIVE)
        {
            cam1st->Pitch(mouseState.y * dt * 2.5f);
            cam1st->RotateY(mouseState.x * dt * 2.5f);
        }
        
    }
    else if (m_CameraMode == CameraMode::ThirdPerson)
    {
        // 第三人稱攝像機的操作

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

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

    // 更新觀察矩陣
    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(woodCrateTransform.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 = woodCrateTransform.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 = woodCrateTransform.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. 嘗試實現帶有父子關系的Transform變換和GameObject

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM