DirectX11 With Windows SDK--01 DirectX11初始化


DirectX11 With Windows SDK完整目录:https://blog.csdn.net/x_jun96/article/details/80293670

由于个人觉得龙书里面第4章提供的Direct3D 初始化项目封装得比较好,而且DirectX SDK Samples里面的初始化程序过于精简,不适合后续使用,故选择了以Init Direct3D项目作为框架,然后还使用了微软提供的示例项目,两者结合到一起。建议下载项目配合阅读。

项目源码点此:https://github.com/MKXJun/DirectX11-With-Windows-SDK

目录

项目结构

该项目包含了下面这些文件

image

其中头文件的具体功能

头文件 功能
d3dApp.h Direct3D应用程序框架类
dxerr.h DirectX错误库
GameApp.h 游戏应用程序扩展类,游戏逻辑在这里实现,继承自D3DApp类
GameTimer.h 游戏计时器类

其中d3dApp.h, d3dApp.cpp, GameTimer.h, GameTimer.cpp是龙书源码提供的,我们可以搬运过来,但是对d3dApp框架类我们还需要进行大幅度修改,毕竟我们的最终目的就是要完全脱离旧的DirectX SDK,使用Windows SDK来实现DX11.

dxerr.h在Windows SDK是没有提供的,我们需要寻找新的dxerr进行替代,在后续会提到

GameApp.h则是我们编写游戏逻辑的地方,这里需要进行逐帧的更新及绘制。

初期配置

链接静态库

这里的每一个项目都需要包含静态库:d3d11.lib,dxgi.lib,dxguid.lib,D3DCompiler.libwinmm.lib。可以在d3dApp.h添加下面的语句:

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "D3DCompiler.lib")
#pragma comment(lib, "winmm.lib")

也可以在项目属性-链接器-输入-附加依赖项 添加上面的库。

移植新的dxerr.h和dxerr.cpp

directx-sdk-samples-master的GitHub地址:https://github.com/walbourn/directx-sdk-samples

在directx-sdk-samples-master\DXUT\Core中可以找到dxerr.hdxerr.cpp,把它们拉进我们的项目中。然后使用下面的宏来进行检查(加在d3dApp.h

#if defined(DEBUG) | defined(_DEBUG)
#ifndef HR
#define HR(x)                                              \
{                                                          \
    HRESULT hr = (x);                                      \
    if(FAILED(hr))                                         \
    {                                                      \
        DXTrace(__FILEW__, (DWORD)__LINE__, hr, L#x, true);\
    }                                                      \
}
#endif

#else
#ifndef HR
#define HR(x) (x)
#endif
#endif 

由于新的dxerr.h仅提供了DXTrace的Unicode字符集版本,需要将原来的__FILE__替换为__FILEW__,并在项目属性页中将字符集设置为Unicode。

COM组件智能指针

考虑到DirectX11的API是由一系列的COM组件来管理的,我们可以使用智能指针来管理这些对象,而无需过多担心内存的泄漏。所以该项目并不会用到接口类ID3D11Debug来协助检查内存泄漏。

使用该智能指针需要包含头文件wrl/client.h,并且智能指针类模板ComPtr位于名称空间Microsoft::WRL内。我们主要关注下面这几个方法:

ComPtr<T>::Get方法返回T*,若需要赋值操作也可以使用重载的=运算符进行

ComPtr<T>::GetAddressOf方法返回T**,也可以用重载的&运算符来获取

ComPtr<T>::Reset方法将对里面的对象调用Release方法,并将指针置为nullptr

GameTimer类

GameTimer类是一个基于高精度时钟频率的计时器,主要用于获取游戏时间和每一帧的间隔时间,并进行一些特殊的操作。下面给出了GameTimer类的声明部分:

class GameTimer
{
public:
    GameTimer();

    float TotalTime()const;     // 总游戏时间
    float DeltaTime()const;     // 帧间隔时间

    void Reset(); // 在消息循环之前调用
    void Start(); // 在取消暂停的时候调用
    void Stop();  // 在暂停的时候调用
    void Tick();  // 在每一帧的时候调用

private:
    double mSecondsPerCount;    // 一个时钟周期经过的秒数
    double mDeltaTime;          // 帧间隔时间

    __int64 mBaseTime;          // 基准时间
    __int64 mPausedTime;        // 暂停的时间
    __int64 mStopTime;          // 停止的时间
    __int64 mPrevTime;          // 上一帧的时间
    __int64 mCurrTime;          // 当前时间

    bool mStopped;              // 是否停止计时
};

构造函数

Windows.h中,提供了QueryPerformanceFrequency函数用于获取当前处理器的时钟频率(1秒经过的时钟周期数),然后我们就可以求出1个时钟周期经过的时间数目了。此时计时器为开启状态。观看构造函数的代码:

GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0), 
  mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
    __int64 countsPerSec;
    QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
    mSecondsPerCount = 1.0 / (double)countsPerSec;
}

GameTimer::Reset方法

GameTimer::Reset方法用于重置当前游戏用时为0,并开启计时器,具体的做法如下:

void GameTimer::Reset()
{
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

    mBaseTime = currTime;
    mPrevTime = currTime;
    mStopTime = 0;
    mStopped  = false;
}

其中,QueryPerformanceCounter函数用于获取当前经过的时钟周期数。当前我们是用它获取的值作为基准时间,然后将暂停的总计时间设置为0,并将暂停状态设置为否。

GameTimer::Start方法

GameTimer::Start方法用于开启计时器计时(设置开始时间),并统计上次暂停的总时间:

void GameTimer::Start()
{
    __int64 startTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);


    // Accumulate the time elapsed between stop and start pairs.
    //
    //                     |<-------d------->|
    // ----*---------------*-----------------*------------> time
    //  mBaseTime       mStopTime        startTime     

    if( mStopped )
    {
        mPausedTime += (startTime - mStopTime); 

        mPrevTime = startTime;
        mStopTime = 0;
        mStopped  = false;
    }
}

若之前曾经暂停过,则需要统计当前暂停经过的时间,并加进总的暂停用时。然后这时停止时间也要归零,并将暂停状态设置为否。

GameTimer::Stop方法

GameTimer::Stop方法用于暂停计时器计时,设置暂停时间点:

void GameTimer::Stop()
{
    if( !mStopped )
    {
        __int64 currTime;
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

        mStopTime = currTime;
        mStopped  = true;
    }
}

GameTimer::Tick方法

GameTimer::Tick方法在计时器开启的时候返回距离上次Tick的间隔时间,若计时器没有开启或者间隔时间为负值,则设间隔时间为0:

void GameTimer::Tick()
{
    if( mStopped )
    {
        mDeltaTime = 0.0;
        return;
    }

    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mCurrTime = currTime;

    // Time difference between this frame and the previous.
    mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

    // Prepare for next frame.
    mPrevTime = mCurrTime;

    if(mDeltaTime < 0.0)
    {
        mDeltaTime = 0.0;
    }
}

GameTimer::TotalTime方法

GameTimer::TotalTime方法返回的是距离上次Reset方法调用到现在,游戏运行的总时间(不包括所有暂停过的时间,单位为秒):

float GameTimer::TotalTime()const
{
    // If we are stopped, do not count the time that has passed since we stopped.
    // Moreover, if we previously already had a pause, the distance 
    // mStopTime - mBaseTime includes paused time, which we do not want to count.
    // To correct this, we can subtract the paused time from mStopTime:  
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime

    if( mStopped )
    {
        return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
    }

    // The distance mCurrTime - mBaseTime includes paused time,
    // which we do not want to count.  To correct this, we can subtract 
    // the paused time from mCurrTime:  
    //
    //  (mCurrTime - mPausedTime) - mBaseTime 
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     mCurrTime

    else
    {
        return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
    }
}

GameTimer::DeltaTime方法

GameTimer::TotalTime方法返回当前Tick和上次Tick之间的时间间隔,单位为秒:

float GameTimer::DeltaTime()const
{
    return (float)mDeltaTime;
}

D3DApp框架类

D3DApp.h展示了框架类的声明:

class D3DApp
{
public:
    D3DApp(HINSTANCE hInstance);    // 在构造函数的初始化列表应当设置好初始参数
    virtual ~D3DApp();

    HINSTANCE AppInst()const;       // 获取应用实例的句柄
    HWND      MainWnd()const;       // 获取主窗口句柄
    float     AspectRatio()const;   // 获取屏幕宽高比

    int Run();                      // 运行程序,进行游戏主循环

    // 框架方法。客户派生类需要重载这些方法以实现特定的应用需求
    virtual bool Init();            // 该父类方法需要初始化窗口和Direct3D部分
    virtual void OnResize();        // 该父类方法需要在窗口大小变动的时候调用
    virtual void UpdateScene(float dt)=0;   // 子类需要实现该方法,完成每一帧的更新
    virtual void DrawScene()=0;             // 子类需要实现该方法,完成每一帧的绘制
    virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    // 窗口的消息回调函数
protected:
    bool InitMainWindow();      // 窗口初始化
    bool InitDirect3D();        // Direct3D初始化

    void CalculateFrameStats(); // 计算每秒帧数并在窗口显示

protected:

    HINSTANCE mhAppInst;        // 应用实例句柄
    HWND      mhMainWnd;        // 主窗口句柄
    bool      mAppPaused;       // 应用是否暂停
    bool      mMinimized;       // 应用是否最小化
    bool      mMaximized;       // 应用是否最大化
    bool      mResizing;        // 窗口大小是否变化
    UINT      m4xMsaaQuality;   // MSAA支持的质量等级

    GameTimer mTimer;           // 计时器

    // DX11
    Microsoft::WRL::ComPtr<ID3D11Device> md3dDevice;                    // D3D11设备
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> md3dImmediateContext;   // D3D11设备上下文
    Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;                  // D3D11交换链
    // DX11.1
    Microsoft::WRL::ComPtr<ID3D11Device1> md3dDevice1;                  // D3D11.1设备
    Microsoft::WRL::ComPtr<ID3D11DeviceContext1> md3dImmediateContext1; // D3D11.1设备上下文
    Microsoft::WRL::ComPtr<IDXGISwapChain1> mSwapChain1;                // D3D11.1交换链
    // 常用资源
    Microsoft::WRL::ComPtr<ID3D11Texture2D> mDepthStencilBuffer;        // 深度模板缓冲区
    Microsoft::WRL::ComPtr<ID3D11RenderTargetView> mRenderTargetView;   // 渲染目标视图
    Microsoft::WRL::ComPtr<ID3D11DepthStencilView> mDepthStencilView;   // 深度模板视图
    D3D11_VIEWPORT mScreenViewport;                                     // 视口

    // 派生类应该在构造函数设置好这些自定义的初始参数
    std::wstring mMainWndCaption;                                       // 主窗口标题
    int mClientWidth;                                                   // 视口宽度
    int mClientHeight;                                                  // 视口高度
};

而在d3dApp.cpp中,可以看到有一个全局变量gd3dApp

namespace
{
    // This is just used to forward Windows messages from a global window
    // procedure to our member function window procedure because we cannot
    // assign a member function to WNDCLASS::lpfnWndProc.
    D3DApp* gd3dApp = 0;
}

设置该全局变量是因为在窗口创建的时候需要绑定一个回调函数,但是我们不可以绑定d3dApp::MainWndProc的成员方法,所以还需要实现一个全局函数用于回调函数的绑定:

LRESULT CALLBACK
MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    // Forward hwnd on because we can get messages (e.g., WM_CREATE)
    // before CreateWindow returns, and thus before mhMainWnd is valid.
    return gd3dApp->MsgProc(hwnd, msg, wParam, lParam);
}

构造函数

在构造函数中,这些参数通常只会设置一次,所以需要在初始化列表中进行修改:

D3DApp::D3DApp(HINSTANCE hInstance)
    : mhAppInst(hInstance),
    mMainWndCaption(L"DirectX11 Initialization"),
    mClientWidth(800),
    mClientHeight(600),
    mhMainWnd(nullptr),
    mAppPaused(false),
    mMinimized(false),
    mMaximized(false),
    mResizing(false),
    m4xMsaaQuality(0),

    md3dDevice(nullptr),
    md3dImmediateContext(nullptr),
    mSwapChain(nullptr),
    mDepthStencilBuffer(nullptr),
    mRenderTargetView(nullptr),
    mDepthStencilView(nullptr)
{
    ZeroMemory(&mScreenViewport, sizeof(D3D11_VIEWPORT));


    // 让一个全局指针获取这个类,这样我们就可以在Windows消息处理的回调函数
    // 让这个类调用内部的回调函数了
    gd3dApp = this;
}

D3DApp::Init方法–初始化

初始化主要完成窗口的创建和Direct3D 11的初始化:

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

    if (!InitDirect3D())
        return false;

    return true;
}

D3DApp::InitMainWindow方法–完成窗口的创建

该方法的代码如下:

bool D3DApp::InitMainWindow()
{
    WNDCLASS wc;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = MainWndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = mhAppInst;
    wc.hIcon = LoadIcon(0, IDI_APPLICATION);
    wc.hCursor = LoadCursor(0, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
    wc.lpszMenuName = 0;
    wc.lpszClassName = L"D3DWndClassName";

    if (!RegisterClass(&wc))
    {
        MessageBox(0, L"RegisterClass Failed.", 0, 0);
        return false;
    }

    // Compute window rectangle dimensions based on requested client area dimensions.
    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, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
    if (!mhMainWnd)
    {
        MessageBox(0, L"CreateWindow Failed.", 0, 0);
        return false;
    }

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

    return true;
}

窗口的创建这里不做过多描述,因为这不是教程的重点部分。有兴趣的可以去MSDN查阅这些函数和结构体的信息。

D3DApp::MsgProc方法–回调函数

D3DApp::MsgProc回调方法的定义如下:

LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        // WM_ACTIVATE is sent when the window is activated or deactivated.  
        // We pause the game when the window is deactivated and unpause it 
        // when it becomes active.  
    case WM_ACTIVATE:
        if (LOWORD(wParam) == WA_INACTIVE)
        {
            mAppPaused = true;
            mTimer.Stop();
        }
        else
        {
            mAppPaused = false;
            mTimer.Start();
        }
        return 0;

        // WM_SIZE is sent when the user resizes the window.  
    case WM_SIZE:
        // Save the new client area dimensions.
        mClientWidth = LOWORD(lParam);
        mClientHeight = HIWORD(lParam);
        if (md3dDevice)
        {
            if (wParam == SIZE_MINIMIZED)
            {
                mAppPaused = true;
                mMinimized = true;
                mMaximized = false;
            }
            else if (wParam == SIZE_MAXIMIZED)
            {
                mAppPaused = false;
                mMinimized = false;
                mMaximized = true;
                OnResize();
            }
            else if (wParam == SIZE_RESTORED)
            {

                // Restoring from minimized state?
                if (mMinimized)
                {
                    mAppPaused = false;
                    mMinimized = false;
                    OnResize();
                }

                // Restoring from maximized state?
                else if (mMaximized)
                {
                    mAppPaused = false;
                    mMaximized = false;
                    OnResize();
                }
                else if (mResizing)
                {
                    // If user is dragging the resize bars, we do not resize 
                    // the buffers here because as the user continuously 
                    // drags the resize bars, a stream of WM_SIZE messages are
                    // sent to the window, and it would be pointless (and slow)
                    // to resize for each WM_SIZE message received from dragging
                    // the resize bars.  So instead, we reset after the user is 
                    // done resizing the window and releases the resize bars, which 
                    // sends a WM_EXITSIZEMOVE message.
                }
                else // API call such as SetWindowPos or mSwapChain->SetFullscreenState.
                {
                    OnResize();
                }
            }
        }
        return 0;

        // WM_EXITSIZEMOVE is sent when the user grabs the resize bars.
    case WM_ENTERSIZEMOVE:
        mAppPaused = true;
        mResizing = true;
        mTimer.Stop();
        return 0;

        // WM_EXITSIZEMOVE is sent when the user releases the resize bars.
        // Here we reset everything based on the new window dimensions.
    case WM_EXITSIZEMOVE:
        mAppPaused = false;
        mResizing = false;
        mTimer.Start();
        OnResize();
        return 0;

        // WM_DESTROY is sent when the window is being destroyed.
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

        // The WM_MENUCHAR message is sent when a menu is active and the user presses 
        // a key that does not correspond to any mnemonic or accelerator key. 
    case WM_MENUCHAR:
        // Don't beep when we alt-enter.
        return MAKELRESULT(0, MNC_CLOSE);

        // Catch this message so to prevent the window from becoming too small.
    case WM_GETMINMAXINFO:
        ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
        ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
        return 0;

    case WM_LBUTTONDOWN:
    case WM_MBUTTONDOWN:
    case WM_RBUTTONDOWN:
        return 0;
    case WM_LBUTTONUP:
    case WM_MBUTTONUP:
    case WM_RBUTTONUP:
        return 0;
    case WM_MOUSEMOVE:
        return 0;
    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

WM_ACTIVATE事件处理窗口激活或无效的情况,若窗口激活,则启动计时器;否则停止计时器。

WM_SIZE 事件处理窗口大小变化的情况。若窗口最小化,进行标记。若窗口已经发生变化,需要调用d3dApp::OnReSize方法处理窗口变化的情况。在窗口大小正在改变的过程中,我们不需要调用刚才的方法(此时窗口边缘可能正在被拖动),否则太影响运行效率。

WM_ENTERSIZEMOVE事件处理窗口正在移动或大小正在变化的情况。这个时候需要暂停计时器。

WM_EXITSIZEMOVE事件处理窗口移动完成或大小变化结束的情况。这个时候需要启动计时器。

其余事件这里不做具体描述,有些事件当前没有处理,但是后续的内容会对这里进行修改。

D3DApp::InitDirect3D方法–初始化Direct3D

一个完整的Direct3D初始化需要经历下面这些步骤:
1. 创建D3D设备、D3D设备上下文、DXGI交换链
2. 获取交换链后备缓冲区的ID3D11Buffer接口对象
3. 创建渲染目标视图ID3D11RenderTargetView,绑定刚才的后备缓冲区
4. 通过D3D设备创建一个ID3D11Texture2D跟后备缓冲区同等宽高的接口对象
5. 创建深度模板视图ID3D11DepthStrenilView,绑定刚才的2D纹理
6. 通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标视图和深度模板视图
7. 在渲染管线的光栅化阶段设置好渲染的视口区域

现在假定你的电脑已经支持DirectX 11,但同时也有可能支持DirectX 11.1。因此在该项目中使用的头文件是d3d11_1.h

要初始化DirectX11,我们需要创建这三样东西:D3D设备、D3D设备上下文和DXGI交换链。

D3D设备包含了创建各种所需资源的方法。

D3D设备上下文负责对缓冲区进行渲染,绑定D3D设备创建的各种资源到不同的渲染管线。

DXGI交换链可以包含两个或多个缓冲区,通常一个用于前端显示,其余的用于后端渲染。前台缓冲区通常是只读的,而后备缓冲区则是我们主要进行渲染的场所。当后备缓冲区渲染完成后,通过呈现方式将前后台缓冲区交换,在屏幕上显示出原来刚绘制好的画面。

这三样东西对应的接口类为:ID3D11DeviceID3D11DeviceContextIDXGISwapChain

而如果支持DirectX11.1的话,则对应的接口类为:ID3D11Device1ID3D11DeviceContext1IDXGISwapChain1,它们分别继承自上面的三个接口类,区别在于额外提供了少数新的接口,并且接口方法的实现可能会有所区别。

D3D11CreateDevice函数–创建D3D设备与D3D设备上下文

创建D3D设备、D3D设备上下文使用如下函数:

HRESULT WINAPI D3D11CreateDevice(
    IDXGIAdapter* pAdapter,         // [In_Opt]适配器
    D3D_DRIVER_TYPE DriverType,     // [In]驱动类型
    HMODULE Software,               // [In_Opt]若上面为D3D_DRIVER_TYPE_SOFTWARE则这里需要提供程序模块
    UINT Flags,                     // [In]使用D3D11_CREATE_DEVICE_FLAG枚举类型
    D3D_FEATURE_LEVEL* pFeatureLevels,  // [In_Opt]若为nullptr则为默认特性等级,否则需要提供特性等级数组
    UINT FeatureLevels,             // [In]特性等级数组的元素数目
    UINT SDKVersion,                // [In]SDK版本,默认D3D11_SDK_VERSION
    ID3D11Device** ppDevice,        // [Out_Opt]输出D3D设备
    D3D_FEATURE_LEVEL* pFeatureLevel,   // [Out_Opt]输出当前应用D3D特性等级
    ID3D11DeviceContext** ppImmediateContext ); //[Out_Opt]输出D3D设备上下文

该函数可以创建DirectX11.1或者DirectX11.0的设备与设备上下文,取决于最终应用的D3D特性等级。

首先需要创建驱动类型数组进行轮询,不过通常大多数情况都会支持D3D_DRIVER_TYPE_HARDWARE,以享受硬件加速带来的效益:

// 驱动类型数组
D3D_DRIVER_TYPE driverTypes[] =
{
    D3D_DRIVER_TYPE_HARDWARE,       // 硬件驱动
    D3D_DRIVER_TYPE_WARP,           // WARP驱动
    D3D_DRIVER_TYPE_REFERENCE,      // 软件驱动
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);

然后就是提供特性等级数组,这里只考虑DirectX11:

// 特性等级数组
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);

最后就会可以创建D3D设备和设备上下文了:

HRESULT hr = S_OK;

// 创建D3D设备 和 D3D设备上下文
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)  
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// 驱动类型数组
D3D_DRIVER_TYPE driverTypes[] =
{
    D3D_DRIVER_TYPE_HARDWARE,
    D3D_DRIVER_TYPE_WARP,
    D3D_DRIVER_TYPE_REFERENCE,
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);

// 特性等级数组
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);

D3D_FEATURE_LEVEL featureLevel;
D3D_DRIVER_TYPE d3dDriverType;
for (UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++)
{
    d3dDriverType = driverTypes[driverTypeIndex];
    hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, featureLevels, numFeatureLevels,
        D3D11_SDK_VERSION, md3dDevice.GetAddressOf(), &featureLevel, md3dImmediateContext.GetAddressOf());

    if (hr == E_INVALIDARG)
    {
        // DirectX 11.0 平台不承认D3D_FEATURE_LEVEL_11_1所以我们需要尝试特性等级11.0
        hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,
            D3D11_SDK_VERSION, md3dDevice.GetAddressOf(), &featureLevel, md3dImmediateContext.GetAddressOf());
    }

    if (SUCCEEDED(hr))
        break;
}

if (FAILED(hr))
{
    MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
    return false;
}

如果支持DirectX11.1的话,featureLevel的结果应该为D3D_FEATURE_LEVEL_11_1,并且md3dDevice指向的是一个包含ID3D11Device1接口的对象,以及md3dImmediateContext指向的是一个包含ID3D11DeviceContext1接口的对象;而如果只支持DirectX11.0的话则为D3D_FEATURE_LEVEL_11_0

IDXGIFactory2::CreateSwapChainForHwnd方法–DirectX11.1创建交换链

如果是DirectX11.1的话,需要先填充DXGI_SWAP_CHAIN_DESC1DXGI_SWAP_CHAIN_FULLSCREEN_DESC这两个结构体:

typedef struct DXGI_SWAP_CHAIN_DESC1
{
    UINT Width;                     // 缓冲区宽度
    UINT Height;                    // 缓冲区高度
    DXGI_FORMAT Format;             // 缓冲区数据格式
    BOOL Stereo;                    // 忽略   
    DXGI_SAMPLE_DESC SampleDesc;    // 采样描述
    DXGI_USAGE BufferUsage;         // 缓冲区用途
    UINT BufferCount;               // 缓冲区数目
    DXGI_SCALING Scaling;           // 忽略
    DXGI_SWAP_EFFECT SwapEffect;    // 交换效果
    DXGI_ALPHA_MODE AlphaMode;      // 忽略
    UINT Flags;                     // 使用DXGI_SWAP_CHAIN_FLAG枚举类型
} DXGI_SWAP_CHAIN_DESC1;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采样数
    UINT Quality;                   // MSAA质量等级
} DXGI_SAMPLE_DESC;

typedef struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC
{
    DXGI_RATIONAL RefreshRate;                  // 刷新率
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;  // 忽略
    DXGI_MODE_SCALING Scaling;                  // 忽略
    BOOL Windowed;                              // 是否窗口化
} DXGI_SWAP_CHAIN_FULLSCREEN_DESC;

typedef struct DXGI_RATIONAL
{
    UINT Numerator;                 // 刷新率分子
    UINT Denominator;               // 刷新率分母
} DXGI_RATIONAL;

填充好后,DirectX11.1使用的创建方法为IDXGIFactory2::CreateSwapChainForHwnd

HRESULT IDXGIFactory2::CreateSwapChainForHwnd(
    IUnknown *pDevice,                      // [In]D3D设备
    HWND hWnd,                              // [In]窗口句柄
    const DXGI_SWAP_CHAIN_DESC1 *pDesc,     // [In]交换链描述1
    const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, // [In]交换链全屏描述,可选
    IDXGIOutput *pRestrictToOutput,         // [In]忽略
    IDXGISwapChain1 **ppSwapChain);         // [Out]输出交换链对象

具体操作在后面一并演示

IDXGIFactory::CreateSwapChain方法–DirectX11创建交换链

如果是DirectX11.0的话,需要先填充DXGI_SWAP_CHAIN_DESC结构体:

typedef struct DXGI_SWAP_CHAIN_DESC
{
    DXGI_MODE_DESC BufferDesc;      // 缓冲区描述
    DXGI_SAMPLE_DESC SampleDesc;    // 采样描述
    DXGI_USAGE BufferUsage;         // 缓冲区用途
    UINT BufferCount;               // 后备缓冲区数目
    HWND OutputWindow;              // 输出窗口句柄
    BOOL Windowed;                  // 窗口化?
    DXGI_SWAP_EFFECT SwapEffect;    // 交换效果
    UINT Flags;                     // 使用DXGI_SWAP_CHAIN_FLAG枚举类型
}   DXGI_SWAP_CHAIN_DESC;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采样数
    UINT Quality;                   // MSAA质量等级
} DXGI_SAMPLE_DESC;

typedef struct DXGI_MODE_DESC
{
    UINT Width;                     // 缓冲区宽度
    UINT Height;                    // 缓冲区高度
    DXGI_RATIONAL RefreshRate;      // 刷新率分数表示法
    DXGI_FORMAT Format;             // 缓冲区数据格式
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;  // 忽略
    DXGI_MODE_SCALING Scaling;      // 忽略
} DXGI_MODE_DESC;

typedef struct DXGI_RATIONAL
{
    UINT Numerator;                 // 刷新率分子
    UINT Denominator;               // 刷新率分母
} DXGI_RATIONAL;

DirectX11.0下使用的创建方法为IDXGIFactory::CreateSwapChain

HRESULT IDXGIFactory::CreateSwapChain(
    IUnknown *pDevice,                  // [In]D3D设备
    DXGI_SWAP_CHAIN_DESC *pDesc,        // [In]交换链描述
    IDXGISwapChain **ppSwapChain);      // [Out]输出交换链对象

根据已有设备类型来创建合适的交换链

了解了前面的操作后,现在我们需要先拿到包含IDXGIFactory1或者IDXGIFactory2接口的对象:

ComPtr<IDXGIDevice> dxgiDevice = nullptr;
ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
ComPtr<IDXGIFactory1> dxgiFactory1 = nullptr;

ComPtr<IDXGIDevice1> dxgiDevice1 = nullptr;
ComPtr<IDXGIAdapter1> dxgiAdapter1 = nullptr;
ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr;

// 为了正确创建 DXGI交换链,首先我们需要获取创建 D3D设备 的 DXGI工厂,否则会引发报错:
// "IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory."
// 从属关系为 DXGI工厂-> DXGI适配器 -> DXGI设备 {D3D11设备}
HR(md3dDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(dxgiDevice.GetAddressOf())));
HR(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()));
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));

这时候可以确定dxgiFactory1包含接口IDXGIFactory1,然后检查它是否包含接口IDXGIFactory2,包含的话就说明支持DirectX11.1,然后获取ID3D11Device1ID3D11DeviceContext1接口对象并创建包含IDXGISwapChain1接口的对象,否则就创建IDXGISwapChain接口的对象:

// 如果包含,则说明支持DX11.1
if (dxgiFactory2 != nullptr)
{
    HR(md3dDevice->QueryInterface(__uuidof(ID3D11Device1), reinterpret_cast<void**>(md3dDevice1.GetAddressOf())));
    HR(md3dImmediateContext->QueryInterface(__uuidof(ID3D11DeviceContext1), reinterpret_cast<void**>(md3dImmediateContext1.GetAddressOf())));
    // 填充各种结构体用以描述交换链
    DXGI_SWAP_CHAIN_DESC1 sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.Width = mClientWidth;
    sd.Height = mClientHeight;
    sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = 1;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    sd.Flags = 0;

    DXGI_SWAP_CHAIN_FULLSCREEN_DESC fd;
    fd.RefreshRate.Numerator = 60;
    fd.RefreshRate.Denominator = 1;
    fd.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    fd.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    fd.Windowed = TRUE;
    // 为当前窗口创建交换链
    HR(dxgiFactory2->CreateSwapChainForHwnd(md3dDevice.Get(), mhMainWnd, &sd, &fd, nullptr, mSwapChain1.GetAddressOf()));
    mSwapChain1->QueryInterface(__uuidof(IDXGISwapChain), reinterpret_cast<void**>(mSwapChain.GetAddressOf()));
}
else
{
    // 填充DXGI_SWAP_CHAIN_DESC用以描述交换链
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferDesc.Width = mClientWidth;
    sd.BufferDesc.Height = mClientHeight;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = 1;
    sd.OutputWindow = mhMainWnd;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    sd.Flags = 0;
    HR(dxgiFactory1->CreateSwapChain(md3dDevice.Get(), &sd, mSwapChain.GetAddressOf()));
}

这时候,如果支持DirectX11.1的话,md3dDevicemd3dDevice1其实都指向同一个对象,md3dImmediateContextmd3dImmediateContext1mSwapChainmSwapChain1也是一样的,区别仅仅在于后者实现了额外的一些接口,问题不大。因此不管是DirectX11.1还是DirectX11.0,后续都主要使用md3dDevicemd3dImmediateContextmSwapChain来进行操作。

设置全屏

默认情况下按ALT+ENTER可以切换成全屏,如果不想要这种操作,可以使用刚才创建的dxgiFactory1,按照下面的方式来调用即可:

dxgiFactory1->MakeWindowAssociation(mhMainWnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES);

这样DXGI就不会监听Windows消息队列,并且屏蔽掉了对接收到ALT+ENTER消息的处理。

在上述操作完成后,最后还调用了D3DApp::OnReSize方法。

D3DApp::OnReSize方法–窗口调整后的操作

无论是初始化操作,还是窗口大小变化后的操作,参与绘制的后备缓冲区和深度模板缓冲区大小都需要重新设置,因此需要重新创建。

ID3D11Resource资源类型

Direct3D 11的资源可以主要分为四个大类,它们都派生自ID3D11Resource

ID3D11Buffer通常用于顶点缓冲区、索引缓冲区等

ID3D11Texture1D通常用于创建1维纹理资源

ID3D11Texture2D通常用于创建2维纹理资源,可用于后备缓冲区

ID3D11Texture3D通常用于创建3维纹理资源

ID3D11View资源视图类型

Direct3D 11的资源视图也可以分为四个大类,它们都派生自ID3D11View

ID3D11RenderTargetView 渲染目标视图通常会绑定一个ID3D11Texture2D的资源,而且通常绑定的是交换链指向的一个后备缓冲区。该视图还需要绑定到渲染管线的输出合并阶段,输出的结果将会写入到所绑定的资源。

ID3D11DepthStencilView 深度模板视图通常会绑定一个ID3D11Texture2D的资源,该资源用于存储深度和模板信息。该视图还需要绑定到渲染管线的输出合并阶段,输出的结果将会写入到所绑定的资源。

ID3D11ShaderResourceView 着色资源视图可以绑定资源,然后将该视图绑定到渲染管线的着色器阶段,使得着色器代码可以访问绑定的资源。

ID3D11UnorderedAccessView 目前还不了解该视图的作用,可能会在后续进行更新补充说明。

IDXGISwapChain::GetBuffer方法–获取后备缓冲区

由于此前我们创建好的交换链已经包含1个后备缓冲区了,在创建渲染目标视图之前我们还需要获取该后备缓冲区:

HRESULT IDXGISwapChain::GetBuffer( 
    UINT Buffer,        // [In]缓冲区索引号,从0到BufferCount - 1
    REFIID riid,        // [In]缓冲区的接口类型ID
    void **ppSurface);  // [Out]获取到的缓冲区

在这里接口类型可以是IDXGISurface(后续会用到)或者ID3D11Texture2D。现在我们需要获取的是ID3D11Texture2D接口类。演示部分在下面。

ID3D11Device::CreateRenderTargetView方法–创建渲染目标视图

使用下面的方法来获取渲染目标视图:

HRESULT ID3D11Device::CreateRenderTargetView( 
    ID3D11Resource *pResource,                      // [In]缓冲区资源
    const D3D11_RENDER_TARGET_VIEW_DESC *pDesc,     // 忽略
    ID3D11RenderTargetView **ppRTView);             // [Out]获取渲染目标视图

因此D3DApp::OnReSize方法前面可以这样写:

assert(md3dImmediateContext);
assert(md3dDevice);
assert(mSwapChain);

if (md3dDevice1 != nullptr)
{
    assert(md3dImmediateContext1);
    assert(md3dDevice1);
    assert(mSwapChain1);
}

// 释放交换链的相关资源
mRenderTargetView.Reset();
mDepthStencilView.Reset();
mDepthStencilBuffer.Reset();

// 重设交换链并且重新创建渲染目标视图
ComPtr<ID3D11Texture2D> backBuffer;
HR(mSwapChain->ResizeBuffers(1, mClientWidth, mClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
HR(mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(md3dDevice->CreateRenderTargetView(backBuffer.Get(), 0, mRenderTargetView.GetAddressOf()));


backBuffer.Reset();

ID3D11Device::CreateTexture2D–创建一个2D纹理

除了渲染目标视图外,我们还需要创建深度模板缓冲区用于深度测试。通过D3D设备可以新建一个缓冲区,但在此之前我们需要先描述该缓冲区的信息:

typedef struct D3D11_TEXTURE2D_DESC
{
    UINT Width;         // 缓冲区宽度
    UINT Height;        // 缓冲区高度
    UINT MipLevels;     // Mip等级
    UINT ArraySize;     // 纹理数组中的纹理数量,默认1
    DXGI_FORMAT Format; // 缓冲区数据格式
    DXGI_SAMPLE_DESC SampleDesc;    // 忽略
    D3D11_USAGE Usage;  // 数据的CPU/GPU访问权限
    UINT BindFlags;     // 使用D3D11_BIND_FLAG枚举来决定该数据的使用类型
    UINT CPUAccessFlags;    // 使用D3D11_CPU_ACCESS_FLAG枚举来决定CPU访问权限
    UINT MiscFlags;     // 使用D3D11_RESOURCE_MISC_FLAG枚举,这里默认0
}   D3D11_TEXTURE2D_DESC;   

填充好后,这时我们就可以用方法ID3D11Device::CreateTexture2D来创建2D纹理:

HRESULT ID3D11Device::CreateTexture2D( 
    const D3D11_TEXTURE2D_DESC *pDesc,          // [In] 2D纹理描述信息
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In] 用于初始化的资源
    ID3D11Texture2D **ppTexture2D);             // [Out] 获取到的2D纹理

ID3D11Device::CreateDepthStencilView方法–创建深度模板视图

HRESULT ID3D11Device::CreateDepthStencilView( 
    ID3D11Resource *pResource,                      // [In] 需要绑定的资源
    const D3D11_DEPTH_STENCIL_VIEW_DESC *pDesc,     // [In] 深度缓冲区描述,这里忽略
    ID3D11DepthStencilView **ppDepthStencilView);   // [Out] 获取到的深度模板视图

ID3D11DeviceContext::OMSetRenderTargets方法–输出合并阶段绑定渲染目标视图和深度模板视图

void ID3D11DeviceContext::OMSetRenderTargets( 
    UINT NumViews,                                      // [In] 视图数目
    ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目标视图数组
    ID3D11DepthStencilView *pDepthStencilView) = 0;     // [In] 深度模板视图

下面演示了如何创建深度模板视图,并将渲染目标视图和深度模板视图绑定到渲染管线的输出合并阶段:

D3D11_TEXTURE2D_DESC depthStencilDesc;

depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

// 要使用 4X MSAA? --需要给交换链设置MASS参数

depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;

depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;

// 创建深度缓冲区以及深度模板视图
HR(md3dDevice->CreateTexture2D(&depthStencilDesc, 0, mDepthStencilBuffer.GetAddressOf()));
HR(md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), 0, mDepthStencilView.GetAddressOf()));


// 将渲染目标视图和深度/模板缓冲区结合到管线
md3dImmediateContext->OMSetRenderTargets(1, mRenderTargetView.GetAddressOf(), mDepthStencilView.Get());

ID3D11DeviceContext::RSSetViewports方法–光栅化阶段设置视口区域

最终我们还需要决定将整个视图输出到窗口特定的范围。因此我们需要使用D3D11_VIEWPORT来设置视口

typedef struct D3D11_VIEWPORT
{
    FLOAT TopLeftX;     // 屏幕左上角起始位置X
    FLOAT TopLeftY;     // 屏幕左上角起始位置Y
    FLOAT Width;        // 宽度
    FLOAT Height;       // 高度
    FLOAT MinDepth;     // 最小深度,必须为0.0f
    FLOAT MaxDepth;     // 最大深度,必须为1.0f
}   D3D11_VIEWPORT;

ID3D11DeviceContext::RSSetViewports方法将设置1个或多个视口:

void ID3D11DeviceContext::RSSetViewports(
    UINT  NumViewports,                     // 视口数目
    const D3D11_VIEWPORT *pViewports);      // 视口数组

将视图输出到整个屏幕需要进行下面的操作:

mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width    = static_cast<float>(mClientWidth);
mScreenViewport.Height   = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;

md3dImmediateContext->RSSetViewports(1, &mScreenViewport);

这些就是D3DApp框架类最主要的部分了,在后续的部分,该框架的代码基本上不会有什么太大的变动。因此后续代码的添加主要在GameApp类实现。

GameApp类

对于一个初始化应用程序来说,目前GameApp类的非常简单:

class GameApp : public D3DApp
{
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();
};

GameApp::DrawScene方法–每帧画面的绘制

ID3D11DeviceContext::ClearRenderTargetView方法–清空需要绘制的缓冲区

在每一帧画面绘制的操作中,我们需要清理一遍渲染目标视图绑定的缓冲区

void ID3D11DeviceContext::ClearRenderTargetView(
    ID3D11RenderTargetView *pRenderTargetView,  // [In]渲染目标视图
    const FLOAT  ColorRGBA[4]);                 // [In]指定覆盖颜色

ID3D11DeviceContext::ClearDepthStencilView方法–清空深度模板缓冲区

同样在进行渲染之前,我们也要清理一遍深度模板缓冲区

void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,  // [In]深度模板视图
    UINT ClearFlags,    // [In]D3D11_CLEAR_FLAG枚举
    FLOAT Depth,        // [In]深度
    UINT8 Stencil);     // [In]模板初始值

IDXGISwapChain::Present方法–前后台缓冲区交换并呈现

完成一切绘制操作后就可以调用该方法了

HRESULT ID3D11DeviceContext::STDMETHODCALLTYPE Present( 
    UINT SyncInterval,  // [In]通常为0
    UINT Flags);        // [In]通常为0

GameApp::DrawScene的实现如下:

void GameApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);
    static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };  // RGBA = (0,0,255,255)
    md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&blue));
    md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

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

最终绘制的效果应该如下:

image

下一篇:

DirectX11 With Windows SDK–02 渲染一个三角形:https://blog.csdn.net/x_jun96/article/details/80298803

该文章后续视情况可能还会有所修改。

猜你喜欢

转载自blog.csdn.net/x_jun96/article/details/80293708