欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

DirectX11 With Windows SDK--10 摄像机类

程序员文章站 2022-10-04 15:52:52
前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境,因此这里将先从摄像机这个专题入手。在这之前,需要复习一下有关世界矩阵和观察矩阵的内容。 项目源码点 ......

前言

DirectX11 With Windows SDK完整目录:

由于考虑后续的项目需要有一个比较好的演示环境,因此这里将先从摄像机这个专题入手。在这之前,需要复习一下有关世界矩阵和观察矩阵的内容。

项目源码点此:

世界矩阵和观察矩阵

若已知物体所在位置\(\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}\]
该矩阵的应用有两种解释方式:

  1. 将物体从世界坐标系的原点搬移到世界矩阵对应的位置,并按其坐标轴做对应朝向和大小的调整
  2. 经过世界变化后物体已经在世界坐标系的对应位置,实际上是做从物体坐标系到世界坐标系的变换

然而现在我们需要做的是从世界坐标系转换到观察空间坐标系,则实际上做的相当于是世界矩阵的逆变换,即\(\mathbf{V}=\mathbf{W}^{-1}\)

\[ \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类的定义如下:

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 GetView() const;
    DirectX::XMMATRIX GetProj() const;
    DirectX::XMMATRIX GetViewProj() const;

    // 设置视锥体
    void SetFrustum(float fovY, float aspect, float nearZ, float farZ);

    // 更新观察矩阵
    virtual void UpdateViewMatrix() = 0;
protected:
    // 摄像机的观察空间坐标系对应在世界坐标系中的表示
    DirectX::XMFLOAT3 mPosition;
    DirectX::XMFLOAT3 mRight;
    DirectX::XMFLOAT3 mUp;
    DirectX::XMFLOAT3 mLook;
    
    // 视锥体属性
    float mNearZ;
    float mFarZ;
    float mAspect;
    float mFovY;
    float mNearWindowHeight;
    float mFarWindowHeight;

    // 观察矩阵和透视投影矩阵
    DirectX::XMFLOAT4X4 mView;
    DirectX::XMFLOAT4X4 mProj;

};

可以看到,无论是什么类型的摄像机,都一定需要包含观察矩阵、投影矩阵以及设置这两个坐标系所需要的一些相关信息。这里面观察矩阵的更新是虚方法,是因为第一人称/*视角摄像机实现和第三人称的不同。

这里只列出视锥体信息的获取方法:

float Camera::GetNearWindowWidth() const
{
    return mAspect * mNearWindowHeight;
}

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

float Camera::GetFarWindowWidth() const
{
    return mAspect * mFarWindowHeight;
}

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

第一人称/*视角摄像机

FirstPersonCamera类的定义如下:

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

    // 设置摄像机位置
    void SetPosition(float x, float y, float z);
    void SetPosition(const DirectX::XMFLOAT3& v);
    // 设置摄像机的朝向
    void 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 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)
{
    mPosition = v;
}

void 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 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(&mPosition, pos);
    XMStoreFloat3(&mLook, L);
    XMStoreFloat3(&mRight, R);
    XMStoreFloat3(&mUp, 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(&mPosition);
    XMVECTOR Right = XMLoadFloat3(&mRight);
    XMVECTOR Dist = XMVectorReplicate(d);
    // DestPos = Dist * Right + SrcPos
    XMStoreFloat3(&mPosition, XMVectorMultiplyAdd(Dist, Right, Pos));
}

void FirstPersonCamera::Walk(float d)
{
    XMVECTOR Pos = XMLoadFloat3(&mPosition);
    XMVECTOR Right = XMLoadFloat3(&mRight);
    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(&mPosition, XMVectorMultiplyAdd(Dist, Front, Pos));
}

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

void FirstPersonCamera::Pitch(float rad)
{
    XMMATRIX R = XMMatrixRotationAxis(XMLoadFloat3(&mRight), rad);
    XMVECTOR Up = XMVector3TransformNormal(XMLoadFloat3(&mUp), R);
    XMVECTOR Look = XMVector3TransformNormal(XMLoadFloat3(&mLook), 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(&mUp, Up);
    XMStoreFloat3(&mLook, Look);
}

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

    XMStoreFloat3(&mRight, XMVector3TransformNormal(XMLoadFloat3(&mRight), R));
    XMStoreFloat3(&mUp, XMVector3TransformNormal(XMLoadFloat3(&mUp), R));
    XMStoreFloat3(&mLook, XMVector3TransformNormal(XMLoadFloat3(&mLook), R));
}

其中上下视野角度Phi、观察轴Y值有如下对应关系:
\[L_{y} = cos(\Phi)\]
Phi为弧度0的时候相当于竖直向上看,Phi为弧度pi时相当于竖直向下看。在本例中将视野角度Phi限制在弧度[2pi/9, 7pi/9]

构造观察矩阵

FirstPersonCamera::UpdateViewMatrix方法首先需要重新规格化、正交化摄像机的右方向轴、上方向轴和前方向轴,然后计算剩余的部分以填充观察矩阵:

void FirstPersonCamera::UpdateViewMatrix()
{
    XMVECTOR R = XMLoadFloat3(&mRight);
    XMVECTOR U = XMLoadFloat3(&mUp);
    XMVECTOR L = XMLoadFloat3(&mLook);
    XMVECTOR P = XMLoadFloat3(&mPosition);

    // 保持摄像机的轴互为正交,且长度都为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(&mRight, R);
    XMStoreFloat3(&mUp, U);
    XMStoreFloat3(&mLook, L);

    mView = {
        mRight.x, mUp.x, mLook.x, 0.0f,
        mRight.y, mUp.y, mLook.y, 0.0f,
        mRight.z, mUp.z, mLook.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;
    // 绕物体垂直旋转
    void RotateX(float rad);
    // 绕物体水平旋转
    void RotateY(float rad);
    // 拉近物体
    void Approach(float dist);
    // 设置并绑定待跟踪物体的位置
    void SetTarget(const DirectX::XMFLOAT3& target);
    // 设置初始距离
    void SetDistance(float dist);
    // 设置最小最大允许距离
    void SetDistanceMinMax(float minDist, float maxDist);
    // 更新观察矩阵
    void UpdateViewMatrix() override;

private:
    DirectX::XMFLOAT3 mTarget;
    float mDistance;
    // 最小允许距离,最大允许距离
    float mMinDist, mMaxDist;
    // 以世界坐标系为基准,当前的旋转角度
    float mTheta;
    float mPhi;
};

该第三人称摄像机同样没有实现碰撞检测,它具有如下功能:

  1. 设置观察目标的位置
  2. 设置与观察目标的距离(限制在合理范围内)
  3. 绕物体进行水平旋转
  4. 绕物体Y轴进行旋转

上述部分具体实现如下:

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

void ThirdPersonCamera::RotateY(float rad)
{
    mTheta = fmod(mTheta - rad, XM_2PI);

}

void ThirdPersonCamera::Approach(float dist)
{
    mDistance += dist;
    // 限制距离在[mMinDist, mMaxDist]之间
    if (mDistance < mMinDist)
        mDistance = mMinDist;
    else if (mDistance > mMaxDist)
        mDistance = mMaxDist;
}

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

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

void ThirdPersonCamera::SetDistanceMinMax(float minDist, float maxDist)
{
    mMinDist = minDist;
    mMaxDist = 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} \]

如果规定鼠标向右旋转时,要看到物体右前方的视野,则摄像机是在绕物体Y轴顺时针旋转,则上面的x和z值应当取负(顺时针为正);如果规定鼠标向上旋转时,要看到物体上方视野,则摄像机是在绕物体右方向轴逆时针旋转,就像是摄像机在逐渐向后和降低那样,则y值应当保持为正。

最后将物体坐标加上,就可以得到摄像机的坐标:
\[\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 = mTarget.x - mDistance * sinf(mPhi) * cosf(mTheta);
    float z = mTarget.z - mDistance * sinf(mPhi) * sinf(mTheta);
    float y = mTarget.y + mDistance * cosf(mPhi);
    mPosition = { x, y, z };
    XMVECTOR P = XMLoadFloat3(&mPosition);
    XMVECTOR L = XMVector3Normalize(XMLoadFloat3(&mTarget) - P);
    XMVECTOR R = XMVector3Normalize(XMVector3Cross(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), L));
    XMVECTOR U = XMVector3Cross(L, R);
    
    // 更新向量
    XMStoreFloat3(&mRight, R);
    XMStoreFloat3(&mUp, U);
    XMStoreFloat3(&mLook, L);

    mView = {
        mRight.x, mUp.x, mLook.x, 0.0f,
        mRight.y, mUp.y, mLook.y, 0.0f,
        mRight.z, mUp.z, mLook.z, 0.0f,
        -XMVectorGetX(XMVector3Dot(P, R)), -XMVectorGetX(XMVector3Dot(P, U)), -XMVectorGetX(XMVector3Dot(P, L)), 1.0f
    };
}

合理对常量缓冲区进行分块

由于项目正在逐渐变得更加庞大,常量缓冲区会频繁更新,但是每次更新常量缓冲区都必须将整个块的内容都刷新一遍,如果只是为了更新里面其中一个变量就要进行一次块的刷新,这样会导致性能上的损耗。所以将常量缓冲区根据刷新频率和类别来进行更细致的分块,可以尽可能保证每一次更新都不会有变量在进行无意义的刷新。因此HLSL常量缓冲区的变化如下:

cbuffer CBChangesEveryDrawing : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
    row_major matrix gTexTransform;
}

cbuffer CBChangesEveryFrame : register(b1)
{
    row_major matrix gView;
    float3 gEyePosW;
}

cbuffer CBChangesOnResize : register(b2)
{
    row_major matrix gProj;
}

cbuffer CBNeverChange : register(b3)
{
    DirectionalLight gDirLight[10];
    PointLight gPointLight[10];
    SpotLight gSpotLight[10];
    Material gMaterial;
    int gNumDirLight;
    int gNumPointLight;
    int gNumSpotLight;
}

对应的C++结构体如下:

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

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

    struct CBChangesOnResize
    {
        DirectX::XMMATRIX proj;
    };

    struct CBNeverChange
    {
        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:
        // 获取位置
        DirectX::XMFLOAT3 GetPosition() const;
        // 设置缓冲区
        void SetBuffer(ComPtr<ID3D11Device> device, const Geometry::MeshData& meshData);
        // 设置纹理
        void SetTexture(ComPtr<ID3D11ShaderResourceView> texture);
        // 设置矩阵
        void SetWorldMatrix(const DirectX::XMFLOAT4X4& world);
        void SetWorldMatrix(DirectX::FXMMATRIX world);
        void SetTexTransformMatrix(const DirectX::XMFLOAT4X4& texTransform);
        void SetTexTransformMatrix(DirectX::FXMMATRIX texTransform);
        // 绘制
        void Draw(ComPtr<ID3D11DeviceContext> deviceContext);
    private:
        DirectX::XMFLOAT4X4 mWorldMatrix;               // 世界矩阵
        DirectX::XMFLOAT4X4 mTexTransform;              // 纹理变换矩阵
        ComPtr<ID3D11ShaderResourceView> mTexture;      // 纹理
        ComPtr<ID3D11Buffer> mVertexBuffer;             // 顶点缓冲区
        ComPtr<ID3D11Buffer> mIndexBuffer;              // 索引缓冲区
        int mIndexCount;                                // 索引数目 
    };

其中原来GameApp::InitResource方法中创建顶点和索引缓冲区的操作都转移到了GameObject::SetBuffer上:

void GameObject::SetBuffer(ComPtr<ID3D11Device> device, const Geometry::MeshData& meshData)
{
    // 释放旧资源
    mVertexBuffer.Reset();
    mIndexBuffer.Reset();

    // 设置顶点缓冲区描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = (UINT)meshData.vertexVec.size() * sizeof(VertexPosNormalTex);
    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, mVertexBuffer.GetAddressOf()));


    // 设置索引缓冲区描述
    mIndexCount = (int)meshData.indexVec.size();
    D3D11_BUFFER_DESC ibd;
    ZeroMemory(&ibd, sizeof(ibd));
    ibd.Usage = D3D11_USAGE_DEFAULT;
    ibd.ByteWidth = sizeof(WORD) * mIndexCount;
    ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
    ibd.CPUAccessFlags = 0;
    // 新建索引缓冲区
    InitData.pSysMem = meshData.indexVec.data();
    HR(device->CreateBuffer(&ibd, &InitData, mIndexBuffer.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(ComPtr<ID3D11DeviceContext> deviceContext)
{
    // 设置顶点/索引缓冲区
    UINT strides = sizeof(VertexPosNormalTex);
    UINT offsets = 0;
    deviceContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &strides, &offsets);
    deviceContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    // 获取之前已经绑定到渲染管线上的常量缓冲区并进行修改
    ComPtr<ID3D11Buffer> cBuffer = nullptr;
    deviceContext->VSGetConstantBuffers(0, 1, cBuffer.GetAddressOf());
    CBChangesEveryDrawing mCBDrawing;
    mCBDrawing.world = XMLoadFloat4x4(&mWorldMatrix);
    mCBDrawing.worldInvTranspose = XMMatrixTranspose(XMMatrixInverse(nullptr, mCBDrawing.world));
    mCBDrawing.texTransform = XMLoadFloat4x4(&mTexTransform);
    deviceContext->UpdateSubresource(cBuffer.Get(), 0, nullptr, &mCBDrawing, 0, 0);
    // 设置纹理
    deviceContext->PSSetShaderResources(0, 1, mTexture.GetAddressOf());
    // 可以开始绘制
    deviceContext->DrawIndexed(mIndexCount, 0, 0);
}

这里会对每次绘制需要更新的常量缓冲区进行修改

GameApp类的变化

GameApp::OnResize方法的变化

由于摄像机保留有设置视锥体的方法,并且需要更新常量缓冲区中的投影矩阵,因此该部分操作需要转移到这里进行:

void GameApp::OnResize()
{
    // 省略...
    D3DApp::OnResize();
    // 省略...
    
    // 摄像机变更显示
    if (mConstantBuffers[2] != nullptr)
    {
        mCamera->SetFrustum(XM_PIDIV2, AspectRatio(), 0.5f, 1000.0f);
        mCBOnReSize.proj = mCamera->GetProj();
        md3dImmediateContext->UpdateSubresource(mConstantBuffers[2].Get(), 0, nullptr, &mCBOnReSize, 0, 0);
        md3dImmediateContext->VSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());
    }
}

GameApp::InitResource方法的变化

该方法创建了墙体、地板和木箱三种游戏物体,然后还创建了多个常量缓冲区,最后渲染管线的各个阶段按需要绑定各种所需资源。这里设置了一个平行光和一盏点光灯:

bool GameApp::InitResource()
{
    // ******************
    // 设置常量缓冲区描述
    D3D11_BUFFER_DESC cbd;
    ZeroMemory(&cbd, sizeof(cbd));
    cbd.Usage = D3D11_USAGE_DEFAULT;
    cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    cbd.CPUAccessFlags = 0;
    // 新建用于VS和PS的常量缓冲区
    cbd.ByteWidth = sizeof(CBChangesEveryDrawing);
    HR(md3dDevice->CreateBuffer(&cbd, nullptr, mConstantBuffers[0].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBChangesEveryFrame);
    HR(md3dDevice->CreateBuffer(&cbd, nullptr, mConstantBuffers[1].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBChangesOnResize);
    HR(md3dDevice->CreateBuffer(&cbd, nullptr, mConstantBuffers[2].GetAddressOf()));
    cbd.ByteWidth = sizeof(CBNeverChange);
    HR(md3dDevice->CreateBuffer(&cbd, nullptr, mConstantBuffers[3].GetAddressOf()));
    // ******************
    // 初始化游戏对象
    ComPtr<ID3D11ShaderResourceView> texture;
    // 初始化木箱
    CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, texture.GetAddressOf());
    mWoodCrate.SetBuffer(md3dDevice, Geometry::CreateBox());
    mWoodCrate.SetWorldMatrix(XMMatrixIdentity());
    mWoodCrate.SetTexTransformMatrix(XMMatrixIdentity());
    mWoodCrate.SetTexture(texture);
    
    // 初始化地板
    CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\floor.dds", nullptr, texture.ReleaseAndGetAddressOf());
    mFloor.SetBuffer(md3dDevice, 
        Geometry::CreatePlane(XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT2(20.0f, 20.0f), XMFLOAT2(5.0f, 5.0f)));
    mFloor.SetWorldMatrix(XMMatrixIdentity());
    mFloor.SetTexTransformMatrix(XMMatrixIdentity());
    mFloor.SetTexture(texture);

    // 初始化墙体
    mWalls.resize(4);
    CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\brick.dds", nullptr, texture.ReleaseAndGetAddressOf());
    // 这里控制墙体四个面的生成
    for (int i = 0; i < 4; ++i)
    {
        mWalls[i].SetBuffer(md3dDevice,
            Geometry::CreatePlane(XMFLOAT3(0.0f, 0.0f, 0.0f), 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);
        mWalls[i].SetWorldMatrix(world);
        mWalls[i].SetTexTransformMatrix(XMMatrixIdentity());
        mWalls[i].SetTexture(texture);
    }
        
    // 初始化采样器状态
    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(md3dDevice->CreateSamplerState(&sampDesc, mSamplerState.GetAddressOf()));

    
    // ******************
    // 初始化常量缓冲区的值
    // 初始化每帧可能会变化的值
    mCameraMode = CameraMode::FirstPerson;
    auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
    mCamera = camera;
    
    camera->LookAt(XMFLOAT3(0.0f, 0.0f, 0.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f));
    mCBFrame.view = mCamera->GetView();
    XMStoreFloat4(&mCBFrame.eyePos, mCamera->GetPositionXM());

    // 初始化仅在窗口大小变动时修改的值
    mCamera->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
    mCBOnReSize.proj = mCamera->GetProj();

    // 初始化不会变化的值
    // 环境光
    mCBNeverChange.dirLight[0].Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mCBNeverChange.dirLight[0].Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
    mCBNeverChange.dirLight[0].Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mCBNeverChange.dirLight[0].Direction = XMFLOAT3(0.0f, -1.0f, 0.0f);
    // 灯光
    mCBNeverChange.pointLight[0].Position = XMFLOAT3(0.0f, 10.0f, 0.0f);
    mCBNeverChange.pointLight[0].Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mCBNeverChange.pointLight[0].Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
    mCBNeverChange.pointLight[0].Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mCBNeverChange.pointLight[0].Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
    mCBNeverChange.pointLight[0].Range = 25.0f;
    mCBNeverChange.numDirLight = 1;
    mCBNeverChange.numPointLight = 1;
    mCBNeverChange.numSpotLight = 0;
    // 初始化材质
    mCBNeverChange.material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mCBNeverChange.material.Diffuse = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
    mCBNeverChange.material.Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 50.0f);


    // 更新不容易被修改的常量缓冲区资源
    md3dImmediateContext->UpdateSubresource(mConstantBuffers[2].Get(), 0, nullptr, &mCBOnReSize, 0, 0);
    md3dImmediateContext->UpdateSubresource(mConstantBuffers[3].Get(), 0, nullptr, &mCBNeverChange, 0, 0);

    // ******************************
    // 设置好渲染管线各阶段所需资源

    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    // 预先绑定各自所需的缓冲区,其中每帧更新的缓冲区需要绑定到两个缓冲区上
    md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());

    md3dImmediateContext->PSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->PSSetConstantBuffers(3, 1, mConstantBuffers[3].GetAddressOf());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, mSamplerState.GetAddressOf());

    return true;
}

GameApp::UpdateScene的变化

使用Mouse类的相对模式来实现间接模式

本来如果Mouse类的相对模式是有用的话是不会有这一小节的,只不过在实际应用的时候发现不管鼠标怎么移动,记录下的x和y永远都是0,然后进到它的Mouse.cpp去调试却发现不管怎样都不会触碰到修改xy值的部分,只有一开始设为0的地方。所以这里不得不自己手工实现一个。

首先需要将游戏窗口居中显示,则还需要去修改d3dApp::InitMainWindow方法,使用GetSystemMetrics函数来获取屏幕分辨率:

bool D3DApp::InitMainWindow()
{
    // 省略窗体注册部分
    
    // 这里获取屏幕分辨率
    int screenWidth = GetSystemMetrics(SM_CXSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYSCREEN);

    // 计算窗口区域
    RECT R = { 0, 0, mClientWidth, mClientHeight };
    AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
    int width = R.right - R.left;
    int height = R.bottom - R.top;
    // 修改的两个参数对应窗口左上角对应屏幕的位置
    mhMainWnd = CreateWindow(L"D3DWndClassName", mMainWndCaption.c_str(),
        WS_OVERLAPPEDWINDOW, (screenWidth - width) / 2, (screenHeight - height) / 2, width, height, 0, 0, mhAppInst, 0);
    if (!mhMainWnd)
    {
        MessageBox(0, L"CreateWindow Failed.", 0, 0);
        return false;
    }

    ShowWindow(mhMainWnd, SW_SHOW);
    UpdateWindow(mhMainWnd);

    // 初始化鼠标,键盘不需要
    mMouse->SetWindow(mhMainWnd);
    mMouse->SetMode(DirectX::Mouse::MODE_ABSOLUTE);
    return true;
}

然后在GameApp::Init方法添加了鼠标的初始化(虽然放在这里可能不是一个最佳的选择):

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

    if (!InitEffect())
        return false;

    if (!InitResource())
        return false;


    // 设置鼠标不可见,并将鼠标位置居中
    mMouse->SetVisible(false);
    POINT center = { mClientWidth / 2, mClientHeight / 2 };
    ClientToScreen(MainWnd(), &center);
    SetCursorPos(center.x, center.y);
    // 初始化鼠标状态
    mMouseTracker.Update(mMouse->GetState());
    return true;
}

这里必须要把鼠标位置居中,并且要初始化鼠标状态。因为第一次的时候鼠标追踪器里面上一帧的X和Y初始值都为0,这样会导致下一帧追踪的时候X和Y的差值过大从而出现问题。

注意设置鼠标的位置是设置全局屏幕的坐标,所以还需要通过ClientToScreen函数来设置从局部窗口坐标到全局屏幕的变换。

最后就可以开始获取相对位移,并根据当前摄像机的模式和键鼠操作的状态来进行对应操作:

void GameApp::UpdateScene(float dt)
{
    // 更新鼠标事件,获取相对偏移量
    Mouse::State mouseState = mMouse->GetState();
    Mouse::State lastMouseState = mMouseTracker.GetLastState();
    POINT center = { mClientWidth / 2, mClientHeight / 2 };
    int dx = mouseState.x - center.x, dy = mouseState.y - center.y;
    mMouseTracker.Update(mouseState);
    Keyboard::State keyState = mKeyboard->GetState();
    mKeyboardTracker.Update(keyState);
    // 固定鼠标位置到窗口中间
    ClientToScreen(MainWnd(), &center);
    SetCursorPos(center.x, center.y);
    // 获取子类
    auto cam1st = std::dynamic_pointer_cast<FirstPersonCamera>(mCamera);
    auto cam3rd = std::dynamic_pointer_cast<ThirdPersonCamera>(mCamera);

    
    if (mCameraMode == CameraMode::FirstPerson || mCameraMode == CameraMode::Free)
    {
        // 第一人称/*摄像机的操作

        // 方向移动
        if (keyState.IsKeyDown(Keyboard::W))
        {
            if (mCameraMode == CameraMode::FirstPerson)
                cam1st->Walk(dt * 3.0f);
            else
                cam1st->MoveForward(dt * 3.0f);
        }   
        if (keyState.IsKeyDown(Keyboard::S))
        {
            if (mCameraMode == 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(), XMVectorReplicate(-8.9f), XMVectorReplicate(8.9f)));
        cam1st->SetPosition(adjustedPos);

        // 仅在第一人称模式移动箱子
        if (mCameraMode == CameraMode::FirstPerson)
            mWoodCrate.SetWorldMatrix(XMMatrixTranslation(adjustedPos.x, adjustedPos.y, adjustedPos.z));
        // 视野旋转,防止开始的差值过大导致的突然旋转
        cam1st->Pitch(dy * dt * 1.25f);
        cam1st->RotateY(dx * dt * 1.25f);
    }
    else if (mCameraMode == CameraMode::ThirdPerson)
    {
        // 第三人称摄像机的操作

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

        // 绕物体旋转
        cam3rd->RotateX(dy * dt * 1.25f);
        cam3rd->RotateY(dx * dt * 1.25f);
        cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);
    }

    // 更新观察矩阵
    mCamera->UpdateViewMatrix();
    XMStoreFloat4(&mCBFrame.eyePos, mCamera->GetPositionXM());
    mCBFrame.view = mCamera->GetView();

    // 重置滚轮值
    mMouse->ResetScrollWheelValue();
    
    // 摄像机模式切换
    if (keyState.IsKeyDown(Keyboard::D1) && mCameraMode != CameraMode::FirstPerson)
    {
        if (!cam1st)
        {
            cam1st.reset(new FirstPersonCamera);
            cam1st->SetFrustum(XM_PIDIV2, AspectRatio(), 0.5f, 1000.0f);
            mCamera = cam1st;
        }
        XMFLOAT3 pos = mWoodCrate.GetPosition();
        XMFLOAT3 target = (!pos.x && !pos.z ? XMFLOAT3{0.0f, 0.0f, 1.0f} : XMFLOAT3{});
        cam1st->LookAt(pos, target, XMFLOAT3(0.0f, 1.0f, 0.0f));
        
        mCameraMode = CameraMode::FirstPerson;
    }
    else if (keyState.IsKeyDown(Keyboard::D2) && mCameraMode != CameraMode::ThirdPerson)
    {
        if (!cam3rd)
        {
            cam3rd.reset(new ThirdPersonCamera);
            cam3rd->SetFrustum(XM_PIDIV2, AspectRatio(), 0.5f, 1000.0f);
            mCamera = cam3rd;
        }
        XMFLOAT3 target = mWoodCrate.GetPosition();
        cam3rd->SetTarget(target);
        cam3rd->SetDistance(8.0f);
        cam3rd->SetDistanceMinMax(3.0f, 20.0f);
        // 初始化时朝物体后方看
        // cam3rd->RotateY(-XM_PIDIV2);
        
        mCameraMode = CameraMode::ThirdPerson;
    }
    else if (keyState.IsKeyDown(Keyboard::D3) && mCameraMode != CameraMode::Free)
    {
        if (!cam1st)
        {
            cam1st.reset(new FirstPersonCamera);
            cam1st->SetFrustum(XM_PIDIV2, AspectRatio(), 0.5f, 1000.0f);
            mCamera = cam1st;
        }
        // 从箱子上方开始
        XMFLOAT3 pos = mWoodCrate.GetPosition();
        XMFLOAT3 look {0.0f, 0.0f, 1.0f};
        XMFLOAT3 up {0.0f, 1.0f, 0.0f};
        pos.y += 3;
        cam1st->LookTo(pos, look, up);

        mCameraMode = CameraMode::Free;
    }
    // 退出程序,这里应向窗口发送销毁信息
    if (keyState.IsKeyDown(Keyboard::Escape))
        SendMessage(MainWnd(), WM_DESTROY, 0, 0);
    
    md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &mCBFrame, 0, 0);
}

其中对摄像机位置使用XMVectorClamp函数是为了将X, Y和Z值都限制在范围为[-8.9, 8.9]的立方体活动区域防止跑出场景区域外,但使用第三人称摄像机的时候没有这样的限制,因为可以营造出一种透视观察的效果。
DirectX11 With Windows SDK--10 摄像机类

GameApp::DrawScene的变化

该方法变化不大,具体如下:

void GameApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);

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

    
    // 绘制几何模型
    mWoodCrate.Draw(md3dImmediateContext);
    mFloor.Draw(md3dImmediateContext);
    for (auto& wall : mWalls)
        wall.Draw(md3dImmediateContext);

    // 绘制Direct2D部分
    md2dRenderTarget->BeginDraw();
    std::wstring text = L"切换摄像机模式: 1-第一人称 2-第三人称 3-*视角\n"
        "W/S/A/D 前进/后退/左平移/右平移 (第三人称无效)  Esc退出\n"
        "鼠标移动控制视野 滚轮控制第三人称观察距离\n"
        "当前模式: ";
    if (mCameraMode == CameraMode::FirstPerson)
        text += L"第一人称(控制箱子移动)";
    else if (mCameraMode == CameraMode::ThirdPerson)
        text += L"第三人称";
    else
        text += L"*视角";
    md2dRenderTarget->DrawTextW(text.c_str(), text.length(), mTextFormat.Get(),Camera01.gif

        D2D1_RECT_F{ 0.0f, 0.0f, 500.0f, 60.0f }, mColorBrush.Get());
    HR(md2dRenderTarget->EndDraw());

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

最后下面演示了三种模式下的操作效果:
DirectX11 With Windows SDK--10 摄像机类

DirectX11 With Windows SDK--10 摄像机类

DirectX11 With Windows SDK--10 摄像机类