DirectX11 With Windows SDK--01 DirectX11初始化
原文:DirectX11 With Windows SDK--01 DirectX11初始化

前言

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

这一章内容大部分属于龙书的内容,但仍有一些不同的地方。因为后续的所有项目都使用该基础框架,你也可以直接使用第一章的项目源码,然后需要了解以下差异部分:

  1. ComPtr智能指针
  2. 新的HR宏
  3. D3D11.1设备的创建

其中前面两个部分在下面的链接可以看到:

章节
ComPtr智能指针
HR宏关于dxerr库的替代方案

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

如何开启新项目

安装准备

如果你打算从VS2015开始,则在安装的时候需要勾选下列选项:

编程语言那一项会自动被勾选。

而如果是使用VS2017或19的话,则在安装的时候需要勾选下列选项:

注意:VS2015使用的Windows SDK版本为10.0.14393.0,而VS2017与VS2019使用的Windows SDK版本为10.0.17763.0

安装完成后,新建项目需要从空项目开始。

移除你的项目中有关DX SDK的库路径和包含路径

如果你曾经用过DX SDK来编写DX项目,务必要把你之前配置的DX SDK库路径和包含路径给清理掉,使用项目默认的库路径和包含路径!

链接静态库

这里的每一个项目都需要包含静态库: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")

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

字符集设置为Unicode

在项目属性页中可以直接进行修改。

Win7系统下的额外配置

由于Win10 SDK中的某些函数在Win7是不支持的,我们还需要在属性页-配置属性-C/C++ -预处理器中,添加预处理器定义以限制API集合:_WIN32_WINNT=0x601

现在最基本的配置已经完成,你可以尝试将本教程用到的项目01中所有的头文件和源文件添加进你的项目,然后生成项目并运行以检测。

项目结构

现在把目光拉回到我们的教程项目。目前项目中包含头文件的具体功能如下:

头文件 功能
d3dApp.h Direct3D应用程序框架类
d3dUtil.h 包含一些常用头文件及自己编写的函数
DXTrace.h 包含了HR宏与DXTraceW函数
GameApp.h 游戏应用程序扩展类,游戏逻辑在这里实现,继承自D3DApp类
GameTimer.h 游戏计时器类

其中d3dApp类和GameTimer类是龙书源码提供的,我们可以搬运过来,但是对d3dApp框架类我们还需要进行大幅度修改,毕竟我们的最终目的就是要完全脱离旧的DirectX SDK,使用Windows SDK来实现DX11。修改完成后,d3dApp就几乎已经定型而不需要我们操心了。

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

D3DApp框架类

D3DApp.h展示了框架类的声明,这里的接口类指针全部换上了ComPtr智能指针:

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 m_hAppInst;        // 应用实例句柄HWND      m_hMainWnd;        // 主窗口句柄bool      m_AppPaused;       // 应用是否暂停bool      m_Minimized;       // 应用是否最小化bool      m_Maximized;       // 应用是否最大化bool      m_Resizing;        // 窗口大小是否变化bool      m_Enable4xMsaa;    // 是否开启4倍多重采样UINT      m_4xMsaaQuality;   // MSAA支持的质量等级GameTimer m_Timer;           // 计时器// 使用模板别名(C++11)简化类型名template <class T>using ComPtr = Microsoft::WRL::ComPtr<T>;// Direct3D 11ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11设备ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11设备上下文ComPtr<IDXGISwapChain> m_pSwapChain;                  // D3D11交换链// Direct3D 11.1ComPtr<ID3D11Device1> m_pd3dDevice1;                  // D3D11.1设备ComPtr<ID3D11DeviceContext1> m_pd3dImmediateContext1; // D3D11.1设备上下文ComPtr<IDXGISwapChain1> m_pSwapChain1;                // D3D11.1交换链// 常用资源ComPtr<ID3D11Texture2D> m_pDepthStencilBuffer;        // 深度模板缓冲区ComPtr<ID3D11RenderTargetView> m_pRenderTargetView;   // 渲染目标视图ComPtr<ID3D11DepthStencilView> m_pDepthStencilView;   // 深度模板视图D3D11_VIEWPORT m_ScreenViewport;                      // 视口// 派生类应该在构造函数设置好这些自定义的初始参数std::wstring m_MainWndCaption;                       // 主窗口标题int m_ClientWidth;                                   // 视口宽度int m_ClientHeight;                                  // 视口高度
};

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

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* g_pd3dApp = 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 m_hMainWnd is valid.return g_pd3dApp->MsgProc(hwnd, msg, wParam, lParam);
}

D3DApp::InitWindowD3DApp::MsgProc方法目前在这里不做过多描述,因为这不是教程的重点部分,但后续可能还要回头修改这两个方法。有兴趣的可以去MSDN查阅这些函数和结构体的信息。

Direct3D初始化

注意当前项目使用的是d3d11_1.h头文件

Direct3D初始化阶段首先需要创建D3D设备D3D设备上下文

D3D设备(ID3D11Device)包含了创建各种所需资源的方法,最常用的有:资源类(ID3D11Resource, 包含纹理和缓冲区)视图类以及着色器

D3D设备上下文(ID3D11DeviceContext)可以看做是一个渲染管线,负责渲染工作,它需要绑定来自D3D设备创建的各种资源、视图和着色器才能正常运转,除此之外,它还能够负责对资源的直接读写操作。

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

现在,我们从D3DApp::InitDirect3D方法开始,一步步进行分析。

D3D设备与D3D设备上下文的创建

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_VERSIONID3D11Device** ppDevice,        // [Out_Opt]输出D3D设备D3D_FEATURE_LEVEL* pFeatureLevel,   // [Out_Opt]输出当前应用D3D特性等级ID3D11DeviceContext** ppImmediateContext ); //[Out_Opt]输出D3D设备上下文
  1. 关于pAdapter(适配器),我们可以将它看做是对显示卡设备的一层封装,通过该参数,我们可以指定需要使用哪个显示卡设备。通常该参数我们设为nullptr,这样就可以交由上层驱动来帮我们决定使用哪个显卡,或者在NVIDIA控制面板来设置当前程序要使用哪个显卡。如果想要在应用层决定,使用IDXGIFactory::EnumAdapters方法可以枚举当前可用的显示卡设备。在最底下的练习题你将学会如何指定显示卡设备来创建Direct3D 11.x设备。
  2. DriverType则指定了驱动类型,不过通常大多数情况都会支持D3D_DRIVER_TYPE_HARDWARE,以享受硬件加速带来的效益。现在我们建立一个驱动数组,然后自己通过for循环的方式进行轮询:
// 驱动类型数组
D3D_DRIVER_TYPE driverTypes[] =
{D3D_DRIVER_TYPE_HARDWARE,       // 硬件驱动D3D_DRIVER_TYPE_WARP,           // WARP驱动D3D_DRIVER_TYPE_REFERENCE,      // 软件驱动
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);

如果D3D_DRIVER_TYPE_HARDWARE不支持,则需要自己通过循环的形式再检查D3D_DRIVER_TYPE_WARP是否支持。

关于D3D_DRIVER_TYPE的详细描述,可以去查阅MSDN官方文档详细了解一下。

  1. Flags对应的是D3D11_CREATE_DEVICE_FLAG枚举值,如果需要D3D设备调试的话(在Debug模式下),可以指定D3D11_CREATE_DEVICE_DEBUG枚举值。指定该值后,可以在出现程序异常的时候观察调试输出窗口的信息。
  2. pFeatureLevels是一个特性等级数组,通过函数内部进行轮询以检测所支持的特性等级:
// 特性等级数组
D3D_FEATURE_LEVEL featureLevels[] =
{D3D_FEATURE_LEVEL_11_1,D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);

注意:如果你的系统支持Direct3D 11.1的API,却把pFeatureLevels设置为nullptrD3D11CreateDevice将创建出特性等级为D3D_FEATURE_LEVEL_11_0的设备。而如果你的系统不支持Direct3D 11.1的API,D3D11CreateDevice会立即停止特性数组的轮询并返回E_INVALIDARG。为此,你必须要从D3D_FEATURE_LEVEL_11_0或更低特性等级开始轮询。

在Win10, Win8.x 或 Win7 SP1且安装了KB2670838补丁的系统都支持Direct3D 11.1的API,而纯Win7系统仅支持Direct3D 11的API

从上面的描述我们可以得知,特性等级D3D设备的版本并不是互相对应的:

1. 特性等级的支持情况取决于当前使用的显示适配器

2. D3D设备的版本取决于所处的系统

由于该函数可以创建Direct3D 11.1(或者Direct3D 11.0)的设备与设备上下文,但都统一输出ID3D11DeviceID3D11DeviceContext。如果想要查看是否支持Direct3D 11.1的API,可以使用下面的方式:

ComPtr<ID3D11Device1> md3dDevice1;
HRESULT hr = md3dDevice.As(&md3dDevice1);

同理,想要查看是否支持Direct3D 11.2的API,则可以这样:

ComPtr<ID3D11Device2> md3dDevice2;
HRESULT hr = md3dDevice.As(&md3dDevice2);

由于每个电脑的显示卡设备情况有所差异,该教程采用的是默认显示卡(有可能会用到集成显卡),而不是指定显示卡:

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, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());if (hr == E_INVALIDARG){// Direct3D 11.0 的API不承认D3D_FEATURE_LEVEL_11_1,所以我们需要尝试特性等级11.0以及以下的版本hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());}if (SUCCEEDED(hr))break;
}if (FAILED(hr))
{MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);return false;
}// 检测是否支持特性等级11.0或11.1
if (featureLevel != D3D_FEATURE_LEVEL_11_0 && featureLevel != D3D_FEATURE_LEVEL_11_1)
{MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);return false;
}// 检测 MSAA支持的质量等级
md3dDevice->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m_4xMsaaQuality);
assert(m_4xMsaaQuality > 0);

注意:

  1. 支持特性等级11_0的显示适配器必然支持所有渲染目标纹理格式的4倍多重采样
  2. 即便m_4xMsaaQuality的返回值为1,也不代表没法启动4倍多重采样,该成员只是代表模式的种类数目

DXGI初始化

DXGI交换链

DXGI交换链(IDXGISwapChain)缓存了一个或多个表面(2D纹理),它们都可以称作后备缓冲区(backbuffer)。后备缓冲区则是我们主要进行渲染的场所,我们可以将这些缓冲区通过合适的手段成为渲染管线的输出对象。在进行呈现(Present)的时候有两种方法:

  1. BitBlt Model(位块传输模型):将后备缓冲区的数据进行BitBlt(位块传输),传入到DX应用中的桌面窗口管理器(DWM)表面,然后进行翻转以交给前台显示。使用这种模型至少需要一个后备缓冲区。事实上,这也是Win32应用程序最常使用的方式,在进行呈现后,渲染管线仍然是对同一个后备缓冲区进行输出。(支持Windows 7及更高版本)
  2. Flip Model(翻转模型):该模型可以避免上一种方式多余的复制,后备缓冲区表面可以直接与桌面窗口管理器(DWM)内的表面进行翻转。但是需要创建至少两个后备缓冲区,并且在每次完成呈现后切换到另一个后备缓冲区进行渲染。该模型可以用于Win32应用程序以及UWP应用程序(需要DXGI1.2,支持Windows 8及更高版本)

注意:考虑到要兼容Win7系统,而且由于我们编写的是Win32应用程序,因此这里使用的是第一种模型。同时这也是绝大多数教程所使用的。对第二种感兴趣的可以了解下面的链接:

DXGI翻转模型

接下来我们需要了解D3D与DXGI各版本的对应关系,这十分重要:

Direct3D API支持版本 对应包含DXGI版本 对应DXGI接口 可枚举的显示适配器 可创建的交换链
Direct3D 11.1 DXGI 1.2 IDXGIFactory2 IDXGIAdaptor2 IDXGISwapChain1
Direct3D 11.0/10.1 DXGI 1.1 IDXGIFactory1 IDXGIAdaptor1 IDXGISwapChain
Direct3D 10.0 DXGI 1.0 IDXGIFactory IDXGIAdaptor IDXGISwapChain

d3d与dxgi版本的对应关系你可以通过观察这些d3d头文件所包含的dxgi头文件来了解。

DXGI交换链的创建需要通过IDXGIFactory::CreateSwapChain方法进行。但是,如果是要创建Direct3D 11.1对应的交换链,则需要通过IDXGIFactory2::CreateSwapChainForHwnd方法进行。

获取IDXGIFactory1或IDXGIFactory2接口类

现在我们需要先拿到包含IDXGIFactory1接口的对象,但是为了拿到该对象还需要经历一些磨难。

之前在创建D3D设备时使用的是默认的显卡适配器IDXGIAdapter(对于双显卡的笔记本大概率使用的是集成显卡),而创建出来的D3D设备本身实现了IDXGIDevice接口,通过该对象,我们可以获取到当前所用的显卡适配器IDXGIAdapter对象,这样我们再通过查询它的父级找到是哪个IDXGIFactory枚举出来的适配器。

ComPtr<IDXGIDevice> dxgiDevice = nullptr;
ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
ComPtr<IDXGIFactory1> dxgiFactory1 = nullptr;   // D3D11.0(包含DXGI1.1)的接口类
ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr;   // D3D11.1(包含DXGI1.2)特有的接口类// 为了正确创建 DXGI交换链,首先我们需要获取创建 D3D设备 的 DXGI工厂,否则会引发报错:
// "IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory."
HR(m_pd3dDevice.As(&dxgiDevice));
HR(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()));
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));// 查看该对象是否包含IDXGIFactory2接口
hr = dxgiFactory1.As(&dxgiFactory2);
// 如果包含,则说明支持D3D11.1
if (dxgiFactory2 != nullptr)
{HR(m_pd3dDevice.As(&m_pd3dDevice1));HR(m_pd3dImmediateContext.As(&m_pd3dImmediateContext1));// ... 省略交换链IDXGISwapChain1的创建
}
else
{// ... 省略交换链IDXGISwapChain的创建
}

同时之前也提到,如果支持Direct3D 11.1的话,我们就可以拿到DXGI 1.2的相关对象(如IDXGIFactory2)。

这时m_pd3dDevicem_pd3dDevice1其实都指向同一个对象,m_pd3dImmediateContextm_pd3dImmediateContext1m_pSwapChainm_pSwapChain1也是一样的,区别仅仅在于后者实现了额外的一些接口,问题不大。因此不管是Direct3D 11.1还是Direct3D 11.0,后续都主要使用m_pd3dDevicem_pd3dImmediateContextm_pSwapChain来进行操作。

IDXGIFactory2::CreateSwapChainForHwnd方法--Direct3D 11.1创建交换链

如果是Direct3D 11.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;

填充好后,Direct3D 11.1使用的创建方法为IDXGIFactory2::CreateSwapChainForHwnd

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

上面第一个省略的部分代码如下:

// 填充各种结构体用以描述交换链
DXGI_SWAP_CHAIN_DESC1 sd;
ZeroMemory(&sd, sizeof(sd));
sd.Width = m_ClientWidth;
sd.Height = m_ClientHeight;
sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// 是否开启4倍多重采样?
if (m_Enable4xMsaa)
{sd.SampleDesc.Count = 4;sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{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(m_pd3dDevice.Get(), m_hMainWnd, &sd, &fd, nullptr, m_pSwapChain1.GetAddressOf()));
HR(m_pSwapChain1.As(&m_pSwapChain));

后续我们还可以通过该交换链来手动指定是否需要全屏

IDXGIFactory::CreateSwapChain方法--Direct3D 11创建交换链

如果是Direct3D 11.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;

Direct3D 11.0下使用的创建方法为IDXGIFactory::CreateSwapChain

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

第二个省略的部分代码如下:

// 填充DXGI_SWAP_CHAIN_DESC用以描述交换链
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = m_ClientWidth;
sd.BufferDesc.Height = m_ClientHeight;
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;
// 是否开启4倍多重采样?
if (m_Enable4xMsaa)
{sd.SampleDesc.Count = 4;sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{sd.SampleDesc.Count = 1;sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.OutputWindow = m_hMainWnd;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
HR(dxgiFactory1->CreateSwapChain(m_pd3dDevice.Get(), &sd, m_pSwapChain.GetAddressOf()));

禁用ALT+ENTER与全屏的关联

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

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

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

DXGI交换链与Direct3D设备的交互

在创建好上述对象后,如果窗口的大小是固定的,则需要经历下面的步骤:

  1. 获取交换链后备缓冲区的ID3D11Texture2D接口对象
  2. 为后备缓冲区创建渲染目标视图ID3D11RenderTargetView
  3. 通过D3D设备创建一个ID3D11Texture2D用作深度/模板缓冲区,要求与后备缓冲区等宽高
  4. 创建深度/模板视图ID3D11DepthStrenilView,绑定刚才创建的2D纹理
  5. 通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标
  6. 在渲染管线的光栅化阶段设置好渲染的视口区域

接下来需要快速了解一遍上述步骤所需要用到的API。

获取交换链的后备缓冲区

由于此前我们创建好的交换链已经包含1个后备缓冲区了,我们可以通过IDXGISwapChain::GetBuffer方法直接获取后备缓冲区的ID3D11Texture2D接口:

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

为后备缓冲区创建渲染目标视图

渲染目标视图用于将渲染管线的运行结果输出给其绑定的资源,很明显它也只能够设置给输出合并阶段。渲染目标视图要求其绑定的资源是允许GPU读写的,因为在作为管线输出时会通过GPU写入数据,并且在以后进行混合操作时还需要在GPU读取该资源。通常渲染目标是一个二维的纹理,但它依旧可能会绑定其余类型的资源。这里不做讨论。

现在我们需要将后备缓冲区绑定到渲染目标视图,使用ID3D11Device::CreateRenderTargetView方法来创建:

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

现在这里演示了获取后备缓冲区纹理,并绑定到渲染目标视图的过程:

// 重设交换链并且重新创建渲染目标视图
ComPtr<ID3D11Texture2D> backBuffer;
HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));

创建深度/模板缓冲区

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

除了渲染目标视图外,我们还需要创建深度/模板缓冲区用于深度测试。深度/模板缓冲区也是一个2D纹理,要求其宽度和高度必须要和窗口宽高保持一致。

通过D3D设备可以新建一个2D纹理,但在此之前我们需要先描述该缓冲区的信息:

typedef struct D3D11_TEXTURE2D_DESC
{UINT Width;         // 缓冲区宽度UINT Height;        // 缓冲区高度UINT MipLevels;     // Mip等级UINT ArraySize;     // 纹理数组中的纹理数量,默认1DXGI_FORMAT Format; // 缓冲区数据格式DXGI_SAMPLE_DESC SampleDesc;    // MSAA采样描述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纹理

下面的代码是关于深度/模板缓冲区创建的完整过程:

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?
if (mEnable4xMsaa)
{depthStencilDesc.SampleDesc.Count = 4;depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{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(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));

创建深度/模板视图

有了深度/模板缓冲区后,就可以通过ID3D11Device::CreateDepthStencilView方法将创建好的2D纹理绑定到新建的深度/模板视图:

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

演示如下:

HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));

为渲染管线的输出合并阶段设置渲染目标

ID3D11DeviceContext::OMSetRenderTargets方法要求同时提供渲染目标视图和深度/模板视图,不过这时我们都已经准备好了:

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

因此这里同样也是一句话的事情:

m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());

视口设置

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

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

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

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

将视图输出到整个屏幕需要按下面的方式进行填充:

m_ScreenViewport.TopLeftX = 0;
m_ScreenViewport.TopLeftY = 0;
m_ScreenViewport.Width    = static_cast<float>(mClientWidth);
m_ScreenViewport.Height   = static_cast<float>(mClientHeight);
m_ScreenViewport.MinDepth = 0.0f;
m_ScreenViewport.MaxDepth = 1.0f;m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);

完成了这六个步骤后,基本的初始化就完成了。但是,如果涉及到窗口大小变化的情况,那么前面提到的后备缓冲区、深度/模板缓冲区、视口都需要重新调整大小。

D3DApp::OnResize方法

已知深度模板缓冲区和视口都可以直接重新创建一份来进行替换。至于后备缓冲区,我们可以通过IDXGISwapChain::ResizeBuffers来重新调整后备缓冲区的大小:

HRESULT IDXGISwapChain::ResizeBuffers(UINT        BufferCount,          // [In]缓冲区数目UINT        Width,                // [In]缓冲区宽度UINT        Height,               // [In]缓冲区高度DXGI_FORMAT NewFormat,            // [In]DXGI格式UINT        SwapChainFlags        // [In]忽略
);

下面的方法演示了在窗口大小发生改变后,以及初次调用时进行的操作:

void D3DApp::OnResize()
{assert(m_pd3dImmediateContext);assert(m_pd3dDevice);assert(m_pSwapChain);if (m_pd3dDevice1 != nullptr){assert(m_pd3dImmediateContext1);assert(m_pd3dDevice1);assert(m_pSwapChain1);}// 释放交换链的相关资源m_pRenderTargetView.Reset();m_pDepthStencilView.Reset();m_pDepthStencilBuffer.Reset();// 重设交换链并且重新创建渲染目标视图ComPtr<ID3D11Texture2D> backBuffer;HR(m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));backBuffer.Reset();D3D11_TEXTURE2D_DESC depthStencilDesc;depthStencilDesc.Width = m_ClientWidth;depthStencilDesc.Height = m_ClientHeight;depthStencilDesc.MipLevels = 1;depthStencilDesc.ArraySize = 1;depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;// 要使用 4X MSAA? --需要给交换链设置MASS参数if (m_Enable4xMsaa){depthStencilDesc.SampleDesc.Count = 4;depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;}else{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(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));// 将渲染目标视图和深度/模板缓冲区结合到管线m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());// 设置视口变换m_ScreenViewport.TopLeftX = 0;m_ScreenViewport.TopLeftY = 0;m_ScreenViewport.Width = static_cast<float>(m_ClientWidth);m_ScreenViewport.Height = static_cast<float>(m_ClientHeight);m_ScreenViewport.MinDepth = 0.0f;m_ScreenViewport.MaxDepth = 1.0f;m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);
}

在后续的部分,该框架的代码基本上不会有什么太大的变动。因此后续代码的添加主要在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]指定覆盖颜色

这里的颜色值范围都是0.0f到1.0f

比如我们要对后备缓冲区(R8G8B8A8)使用蓝色进行清空,可以这样写:

float blue[4] = {0.0f, 0.0f, 1.0f, 1.0f}
m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);

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

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

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

若要清空深度缓冲区,则需要指定D3D11_CLEAR_DEPTH,模板缓冲区则是D3D11_CLEAR_STENCIL

每一次清空我们需要将深度值设为1.0f,模板值设为0.0f。其中深度值1.0f表示距离最远处:

m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

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

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

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

GameApp::DrawScene的实现如下:

void GameApp::DrawScene()
{assert(m_pd3dImmediateContext);assert(m_pSwapChain);static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };  // RGBA = (0,0,255,255)m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);HR(m_pSwapChain->Present(0, 0));
}

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

程序退出后的清理

因为之前我们用的是智能指针,所以D3DApp的析构函数十分简单,只需要通过ID3D11DeviceContext::ClearState方法来恢复D3D设备上下文到默认状态,卸下所有绑定的资源即可。剩下的事情就交给COM智能指针完成:

D3DApp::~D3DApp()
{// 恢复所有默认设定if (m_pd3dImmediateContext)m_pd3dImmediateContext->ClearState();
}

练习题

粗体字为自定义题目

  1. 尝试修改项目代码,让窗口内的显示变红。
  2. 某些电脑可能有多于一个的显示适配器(显卡)。首先要通过CreateDXGIFactory创建IDXGIFactory,然后使用IDXGIFactory::EnumAdapters来枚举显示适配器。尝试通过这种方式查看你的电脑有多少个显示适配器(IDXGIAdapter),并察看它们的信息。
  3. 一个显示适配器可能关联了多个输出设备(IDXGIOutput),你可以使用IDXGIAdapter::EnumOutputs方法来枚举出特定的输出,尝试观察它们的信息。
  4. 对于给定的像素格式,一个输出设备可以支持许多种显示模式(DXGI_MODE_DESC),通过它可以看到全屏宽度、高度、刷新率。尝试使用IDXGIOutput::GetDisplayModeList方法观察所有支持的模式(传递DXGI_FORMAT_R8G8B8A8_UNORM格式进去)。
  5. 默认情况下的窗口程序是可以通过ALT+ENTER来进入/退出全屏的。此外,我们可以通过IDXGISwapChain来动态设置窗口全屏属性,找到对应的方法并尝试一下。
  6. 现在尝试指定显示适配器来创建D3D设备。通过CreateDXGIFactory函数来创建IDXGIFactory,通常它会包含接口IDXGIFactory1,但有可能它也会包含接口IDXGIFactory2。在没有创建D3D设备的情况下,这种方式就可以帮助我们了解是否可以创建出Direct3D 11.1的设备。为了能够指定显示适配器来创建D3D11设备,我们需要将D3D_DRIVER_TYPE强行设置为D3D_DRIVER_TYPE_UNKNOWN,否则在创建设备的时候会得到如下报错信息:DX ERROR: D3D11CreateDevice: When creating a device from an existing adapter (i.e. pAdapter is non-NULL), DriverType must be D3D_DRIVER_TYPE_UNKNOWN. [ INITIALIZATION ERROR #3146141: ]

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

posted on 2019-05-05 09:46 NET未来之路 阅读(...) 评论(...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/10811308.html

DirectX11 With Windows SDK--01 DirectX11初始化相关推荐

  1. DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用

    DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用 原文:DirectX11 With Windows SDK--24 Render- ...

  2. DirectX11 With Windows SDK--36 延迟渲染基础

    前言 随着图形硬件变得越来越通用和可编程化,采用实时3D图形渲染的应用程序已经开始探索传统渲染管线的替代方案,以避免其缺点.其中一项最流行的技术就是所谓的延迟渲染.这项技术主要是为了支持大量的动态灯光 ...

  3. DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果

    DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果 原文:DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果 前言 上一 ...

  4. DirectX11 With Windows SDK--12 深度/模板状态、平面镜反射绘制

    前言 深度/模板测试使用的是与后备缓冲区同等分辨率大小的缓冲区,每个元素的一部分连续位用于深度测试,其余的则用作模板测试.两个测试的目的都是为了能够根据深度/模板状态需求的设置来选择需要绘制的像素. ...

  5. DirectX11 With Windows SDK--07 添加光照与常用几何模型、光栅化状态

    DirectX11 With Windows SDK--07 添加光照与常用几何模型.光栅化状态 原文:DirectX11 With Windows SDK--07 添加光照与常用几何模型.光栅化状态 ...

  6. DirectX11 With Windows SDK--03 索引缓冲区、常量缓冲区

    前言 一个立方体有8个顶点,然而绘制一个立方体需要画12个三角形,如果按照前面的方法绘制的话,则需要提供36个顶点,而且这里面的顶点数据会重复4次甚至5次.这样的绘制方法会占用大量的内存空间. 接下来 ...

  7. DirectX11 With Windows SDK--15 几何着色器初探

    DirectX11 With Windows SDK--15 几何着色器初探 原文:DirectX11 With Windows SDK--15 几何着色器初探 前言 从这一部分开始,感觉就像是踏入了 ...

  8. DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)

    前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: C a = A L ⊗ m ...

  9. DirectX11 With Windows SDK--06 键盘和鼠标输入

    文章目录 前言 鼠标输入 鼠标状态追踪 键盘输入 键盘状态追踪 D3DApp::MsgProc方法的变化 前言 提供键鼠输入可以说是一个游戏的必备要素.在这里,我们不使用DirectInput,而是使 ...

最新文章

  1. Iterator(迭代器)的一般用法
  2. 05.multi_search_template
  3. leetcode : 基础技巧
  4. 我心目中未来的计算机200字,我心目中未来的计算机.doc
  5. 《对不队》团队项目用户验收评审
  6. !doctype html报错h5,【Web前端问题】javascript import 报错
  7. 开发中常见的@NotNull,@NotBlank,@NotEmpty注解的区别
  8. vue项目中简单进行axios封装及响应状态码提示!
  9. element ui 分页增删改查_【新插件发布】AzureAD运维Excel版插件,增删改查快10倍c以上!...
  10. vscode 设置指南
  11. Openstack 笔记概要
  12. python怎么看待_如何看待将Python作为少儿编程的基础语言?
  13. 未能加载文件或程序集“****”或它的某一个依赖项。试图加载格式不正确的程序。解决方案总结...
  14. BGSOUND与EMBED的区别
  15. FPGA入门例程:LED
  16. 海量图片存储与运算架构
  17. OpenCV动作识别
  18. ajax removeclass,我执行ajax后怎么不能$(this).removeClass(disabled);
  19. Linux文件权限与目录配置
  20. 小龙女的真面目和杨过跳崖真相--其实不想走!

热门文章

  1. 从 WordCount 到 MapReduce 计算模型
  2. 优先级调度算法(C++实现)
  3. L2-001 紧急救援-团体程序设计天梯赛GPLT
  4. mysql 创建job_MySQL 索引创建
  5. linux多个客户端如何通信_linux实现多个客户端通信进阶
  6. 时问轴php,php-发布到时间轴-过去的日期
  7. 启动延时缩短 50%-80%,函数计算发布镜像加速功能
  8. Sentinel Go 0.4.0 发布,支持热点流量防护能力
  9. 不吹不黑,今天我们来聊一聊 Kubernetes 落地的三种方式
  10. 清理计算机英语是什么,如何清理电脑(国外英文资料).doc