DirectX11入门到实践-(1)窗口篇

这里以我做过的DirectX11游戏Demo为例,介绍DX11是如何实践的,GitHub地址
项目可能要设置一下环境,配置d3d11.lib等的linker

Basic最基础部分
另推荐,DirectX11 With Windows SDK 教程

一、设置Win32窗口
首先,开始DX11项目,我们要选择创建win32窗口程序,而非控制台程序

窗口Main函数
在Win32中,你需要定义wWinMain函数,作为程序的入口:
[Main.cpp]
#include "Basic/Engine.h"

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
ErrorLogger::Log(hr, "Failed to call CoInitialize.");
return -1;
}
// 运行程序窗口
Engine engine;
engine.Initialize(hInstance, "RacingGame", "RacWindowClass", 800, 600); // 建立窗口对象
while (engine.ProcessMessages() == true) // 主循环部分,详见后文
{
engine.Update();
engine.RenderFrame();
}
return 0;
}
与Console的main()不同,WinMain由事件驱动,如用户单击鼠标,或按键或计时器达到零之类的事件。当发生任何这些事件时,Windows会在消息中记录发生的事件,并将消息放入消息所在的程序的消息队列中。而我们开发的DX程序就基于这个

这里Engine,其实是我们对Win窗口逻辑的封装,本质如下:
[Engine.h]
#pragma once
#include "WindowContent.h"
#include "Timer.h"

class Engine : public WindowContent
{
public:
bool Initialize(HINSTANCE hInstance, std::string windowTitle, std::string windowClass, int width, int height);
void Update();
void RenderFrame();
private:
Timer timer;
float cameraSpeed = 0.006f;
float carSpeed = 0.003f;
float carRotSpeed = 0.003f;
};

可见主要是提供了一些生命周期接口,而WindowContent的成员有:
[WindowContent.h]
#pragma once
#include "ErrorLogger.h"
#include "..\Input\Keyboard.h"
#include "..\Input\Mouse.h"
#include "..\Graphics\Graphics.h"

class WindowContent
{
public:
WindowContent();
LRESULT WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
bool ProcessMessages();
HWND GetHWND() const;
~WindowContent();
protected:
bool virtual Initialize(WindowContent* pWindowContainer, HINSTANCE hInstance, std::string windowTitle, std::string windowClass, int width, int height);
Keyboard keyboard;
Mouse mouse;
Graphics graphics;
private:
void RegisterWindowClass(); // 注册窗口类
HWND handle = NULL;
HINSTANCE hInstance = NULL;
std::string windowTitle = "";
std::wstring windowTitleWide = L"";
std::string windowClass = "";
std::wstring windowClassWide = L"";
int width = 0;
int height = 0;
};

首先,定义Engine engine,会触发构造函数
WindowContent::WindowContent()
{
// 设置原始输入
static bool rawInputInitialized = false;
if (rawInputInitialized == false)
{
RAWINPUTDEVICE rid;
rid.usUsagePage = 0x01; // Mouse
rid.usUsage = 0x02;
rid.dwFlags = 0;
rid.hwndTarget = NULL;
if (RegisterRawInputDevices(&rid, 1, sizeof(rid)) == FALSE)
{
ErrorLogger::Log(GetLastError(), "Failed to register raw input devices.");
exit(-1);
}
rawInputInitialized = true;
}
}

WindowContent::~WindowContent()
{
if (this->handle != NULL)
{
UnregisterClass(this->windowClassWide.c_str(), this->hInstance);
DestroyWindow(handle);
}
}
然后,就可以由Initialize进行win32窗口设置了

窗口初始化
注册窗口类
[WindowContent.cpp]
void WindowContent::RegisterWindowClass()
{
WNDCLASSEX wc; // 定义窗口类,接下来我们修改其数据成员
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = HandleMessageSetup; // 关键设置,让窗口消息转发到我们自定义函数中处理
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = this->hInstance;
wc.hIcon = NULL;
wc.hIconSm = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = this->windowClassWide.c_str();
wc.cbSize = sizeof(WNDCLASSEX);
RegisterClassEx(&wc); // 最终注册窗口类
}

新建窗口对象并显示
bool WindowContent::Initialize(WindowContent * pWindowContainer, HINSTANCE hInstance, std::string windowTitle, std::string windowClass, int width, int height)
{
this->hInstance = hInstance; // h表示handle
this->width = width;
this->height = height;
this->windowTitle = windowTitle;
this->windowTitleWide = StringConverter::StringToWstring(this->windowTitle);
this->windowClass = windowClass;
this->windowClassWide = StringConverter::StringToWstring(this->windowClass);
// 注册窗口类
this->RegisterWindowClass();
// 创建窗口返回Windows将分配此新窗口的句柄
this->handle = CreateWindowEx(0, this->windowClassWide.c_str(), this->windowTitleWide.c_str(), WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU, 0, 0, this->width, this->height, NULL, NULL, this->hInstance, pWindowContainer);
if (this->handle == NULL)
{
ErrorLogger::Log(GetLastError(), "CreateWindowEX Failed for window: " + this->windowTitle);
return false;
}
// 将窗口显示在屏幕上,并将其设置为主焦点
ShowWindow(this->handle, SW_SHOW);
SetForegroundWindow(this->handle);
SetFocus(this->handle);
return true;
}

RECT rc = { 0, 0, 800, 600 };
// 可以用AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE)调整窗口大小
句柄是一个int32,用于标识该窗口是哪一个窗口(id, 指针变种)

检索和分发此窗口的消息
// Called Every Time the Application Receives a Message
LRESULT CALLBACK HandleMessageSetup(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_NCCREATE:
{
const CREATESTRUCTW* const pCreate = reinterpret_cast<CREATESTRUCTW*>(lParam);
WindowContent* pWindow = reinterpret_cast<WindowContent*>(pCreate->lpCreateParams);
if (pWindow == nullptr)
{
ErrorLogger::Log("Critical Error: Pointer to window container is null during WM_NCCREATE.");
exit(-1);
}
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pWindow));
SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(HandleMsgRedirect));
return pWindow->WindowProc(hwnd, uMsg, wParam, lParam);
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam); // 交给Win窗口默认消息处理
}
}

// 这里有将wnd的部分消息交给HandleMsgRedirect处理
LRESULT CALLBACK HandleMsgRedirect(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
default:
{
// 检索ptr到窗口类
WindowContent* const pWindow = reinterpret_cast<WindowContent*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
// 将消息转发到窗口类处理程序
return pWindow->WindowProc(hwnd, uMsg, wParam, lParam);
}
}
}

而 WindowProc() switch处理 Keyboard 和 Mouse Messages,如:
case WM_KEYDOWN:
{
unsigned char keycode = static_cast<unsigned char>(wParam);
if (keyboard.IsKeysAutoRepeat())
keyboard.OnKeyPressed(keycode);
else
{
const bool wasPressed = lParam & 0x40000000;
if (!wasPressed)
keyboard.OnKeyPressed(keycode);
}
return 0;
}
case WM_MOUSEMOVE:
{
int x = LOWORD(lParam);
int y = HIWORD(lParam);
mouse.OnMouseMove(x, y);
return 0;
}
这些是设置每个Windows应用程序所需的窗口对象所需的最小步骤。编译并运行此代码,我们将看到一个空白背景的窗口。

主循环部分
bool WindowContent::ProcessMessages()
{
MSG msg; // Windows事件消息结构
ZeroMemory(&msg, sizeof(MSG)); // 将整个内存块初始化为NULL
if (PeekMessage(&msg, this->handle, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg); // 将某些按键转换为正确格式的函数
DispatchMessage(&msg); // 将消息发送到WindowProc函数
}
// 检查窗口是否关闭
if (msg.message == WM_NULL) // 窗口关闭
{
if (!IsWindow(this->handle))
{
this->handle = NULL;
UnregisterClass(this->windowClassWide.c_str(), this->hInstance); // 释放窗口
return false;
}
}
return true; // 返回成功我们就进行后续Tick逻辑
}
这里不用GetMessage,而是用PeekMessage,因为后者只是查看消息队列,并检查是否有任何消息在等待。如果没有,程序将继续,而不需要像GetMessage一样等待

我们的事件消息处理过程如下图:
我们是通过事件来接受设备输入的,因此只要对事件做一个封装即可,当触发事件时,调用我们编写的keyboard类或Mouse类代码。
如触发WM_KEYDOWN事件,我们根据其参数wParam获得keycode,再调用keyboard.OnKeyPressed(keycode)进行按键后续处理。即可以用if (keyboard.KeyIsPressed('A'))判断是否按下A键,也可以通过事件绑定执行输入事件(通常情况下前者就足够了)。
附:消息框函数
MessageBox(NULL, L"Hello DirectX!", L"DxWindow32", MB_ICONEXCLAMATION | MB_OK);

二、设置设备、上下文和交换链等
如果我们要渲染任何3D场景,则必须进行设置。首先要做的是创建三个对象:设备,直接上下文和交换链

前面介绍了Engine,但其实我们的渲染部分,都是交给Graphics来做的,Init和RenderFrame最终都是调用Graphics的,其内容包括
[Graphics.h]

class Graphics
{
public:
bool Initialize(HWND hwnd, int width, int height);
void RenderFrame();
Camera camera;
Light light;
Model plane;
Skybox skybox;
Car car;
private:
bool InitializeDirectX(HWND hwnd);
bool InitializeShaders();
bool InitializeScene();
// 设备,上下文,交换链,渲染目标视图,深度模板,光栅化,混合,采样器
Microsoft::WRL::ComPtr<ID3D11Device> device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> deviceContext;
Microsoft::WRL::ComPtr<IDXGISwapChain> swapchain;
Microsoft::WRL::ComPtr<ID3D11RenderTargetView> renderTargetView;
Microsoft::WRL::ComPtr<ID3D11DepthStencilView> depthStencilView;
Microsoft::WRL::ComPtr<ID3D11Texture2D> depthStencilBuffer;
Microsoft::WRL::ComPtr<ID3D11DepthStencilState> depthStencilState;
Microsoft::WRL::ComPtr<ID3D11RasterizerState> rasterizerState;
Microsoft::WRL::ComPtr<ID3D11RasterizerState> rasterizerState_CullFront;
Microsoft::WRL::ComPtr<ID3D11BlendState> blendState;
Microsoft::WRL::ComPtr<ID3D11SamplerState> samplerState;
VertexShader vertexshader;
PixelShader pixelshader;
ConstantBuffer<CB_VS_VertexShader> cb_vs_vertexshader;
ConstantBuffer<CB_PS_PixelShader> cb_ps_pixelshader;
std::unique_ptr<DirectX::SpriteBatch> spriteBatch;
std::unique_ptr<DirectX::SpriteFont> spriteFont;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> planeTexture;
int windowWidth = 0;
int windowHeight = 0;
Timer fpsTimer;
};

首选科普一下-COM
COM代表组件对象模型。COM是一种创建非常高级对象的方法,实际上它们与积木很相似。实际上是C++类或类组,你可以从中调用函数并实现某些目标。

DirectX API基于组件对象模型COM。COM对象由一些暴露了方法的接口集组成,提供给开发者访问DirectX
类似于C++对象,COM对象要求使用接口来访问内部实现的方法。相对于标准对象的一个实际的好处是在COM对象内部一个接口有不同版本的实现,从而允许向下兼容性。

Direct3D是一个COM对象,其中包含其他COM对象。如ID3D11Device、IDXGISwapChain这些以 I 开头的对象

ComPtr智能指针
本教程中大量使用了ComPtr智能指针,其可以帮助我们来管理这些COM组件实现的接口实例,而无需过多担心内存的泄漏。
该智能指针的大小和一般的指针大小是一致的,没有额外的内存空间占用。所以本教程可以不需要用到接口类ID3D11Debug来协助检查内存泄漏。
使用该智能指针需要包含头文件wrl/client.h,并且智能指针类模板ComPtr位于名称空间Microsoft::WRL内。
3a5927993c22cd663d37a89dcf2e2f82.png
DirectX图形基础设施(DXGI)
DirectX的图形架构 是位于所有最新版本的Direct3D的基础组件。它的工作是处理基本任务,例如在屏幕上显示图像以及找出显示器和视频卡可以处理的分辨率。

DXGI实际上不是Direct3D的一部分。它是其他图形组件的基础,它充当Direct3D和硬件之间的接口。
用DXGI,来实现以下:
设备 ID3D11Device:包含了创建各种所需资源的方法,最常用的有:资源类(ID3D11Resource, 包含纹理和缓冲区),视图类以及着色器(可以理解设备为GPU)。

上下文 ID3D11DeviceContext:可以看做是一个渲染管线,D3D使用它来执行渲染到缓冲区。它需要绑定来自D3D设备创建的各种资源、视图和着色器才能正常运转(如深度模板、输入布局、图元类型、光栅化、混合、纹理等)。

交换链:什么是交换链?
为了避免屏幕撕裂(GPU比显示器快),DXGI实现了一个名为交换的功能。
交换链负责获取设备呈现的缓冲区,并在实际监视器屏幕上显示内容交换链包含两个或多个缓冲区,主要是前端和后端。这些是设备呈现的纹理,以便在监视器上显示。
  • 前缓冲区是当前向用户呈现的内容。此缓冲区是只读的,无法修改。
  • 后台缓冲区是设备将绘制的渲染目标一旦完成绘图操作,交换链将通过交换两个缓冲区来显示后备缓冲区(指针切换)后缓冲区成为前缓冲区,反之亦然。
通过添加额外的后台缓冲区,我们可以让我们的游戏有更好的性能
此设置称为交换链,因为它是一个缓冲区链,每次渲染新帧时交换位置(IDXGISwapChain)。

一般用D3D11CreateDevice函数创建D3D设备与D3D设备上下文,但D3D11CreateDeviceAndSwapChain
还能同时创建交换链:
bool Graphics::InitializeDirectX(HWND hwnd)
{
try
{
std::vector<AdapterData> adapters = AdapterReader::GetAdapters();
if (adapters.size() < 1)
{
ErrorLogger::Log("No IDXGI Adapters found.");
return false;
}
// 交换链描述
DXGI_SWAP_CHAIN_DESC scd = { 0 };
scd.BufferDesc.Width = this->windowWidth; // 设置窗口大小
scd.BufferDesc.Height = this->windowHeight;
scd.BufferDesc.RefreshRate.Numerator = 60;
scd.BufferDesc.RefreshRate.Denominator = 1;
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
scd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
scd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
scd.SampleDesc.Count = 1; // 采样一次
scd.SampleDesc.Quality = 0; // 不启用多次采样
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 告知我们想渲染到该缓冲区
scd.BufferCount = 1;
scd.OutputWindow = hwnd; // 表示在该窗口屏幕上显示
scd.Windowed = TRUE;
scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 允许窗口/全屏切换

HRESULT hr;
hr = D3D11CreateDeviceAndSwapChain(adapters[0].pAdapter, D3D_DRIVER_TYPE_UNKNOWN, NULL, NULL, NULL, 0, D3D11_SDK_VERSION, &scd, this->swapchain.GetAddressOf(), this->device.GetAddressOf(), NULL, this->deviceContext.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create device and swapchain.");
...
这里支持了用Alt-Enter进行窗口/全屏切换,也可以调用 swapchain-> SetFullscreenState(FALSE,NULL) 用代码强制切换到窗口模式


解释一下这里出现的适配器Adapter
为显卡、API适配而提供,通过DXGIFactory对每一个adapter进行包装(成AdapterData),后续再取用
[Adapter.hpp]
#include <d3d11.h>
#include <wrl/client.h>
#include <vector>

class AdapterData
{
public:
AdapterData(IDXGIAdapter* pAdapter)
{
this->pAdapter = pAdapter;
HRESULT hr = pAdapter->GetDesc(&this->description);
if (FAILED(hr))
{
ErrorLogger::Log(hr, "Failed to Get Description for IDXGIAdapter.");
}
}
IDXGIAdapter* pAdapter = nullptr;
DXGI_ADAPTER_DESC description;
};

class AdapterReader
{
private:
static std::vector<AdapterData> adapters;
public:
static std::vector<AdapterData> GetAdapters()
{
// If already initialized
if (adapters.size() > 0)
return adapters;
Microsoft::WRL::ComPtr<IDXGIFactory> pFactory;
// Create a DXGIFactory object.
HRESULT hr = CreateDXGIFactory(__uuidof(IDXGIFactory), reinterpret_cast<void**>(pFactory.GetAddressOf()));
if (FAILED(hr))
{
ErrorLogger::Log(hr, "Failed to create DXGIFactory for enumerating adapters.");
exit(-1);
}
IDXGIAdapter* pAdapter;
UINT index = 0;
while (SUCCEEDED(pFactory->EnumAdapters(index, &pAdapter)))
{
adapters.push_back(AdapterData(pAdapter));
index += 1;
}
return adapters;
}
};

Logger 日志类
一般用宏来判定HRESULT的结果,以得知是否成功执行代码,所以可以封装为一个Log类
[ErrorLogger.hpp]
#define NOMINMAX
#include "COMException.h"
#include <Windows.h>
class ErrorLogger
{
public:
static void Log(std::string message)
{
std::string error_message = "Error: " + message;
MessageBoxA(NULL, error_message.c_str(), "Error", MB_ICONERROR);
}
static void Log(HRESULT hr, std::string message)
{
_com_error error(hr);
std::wstring error_message = L"Error: " + StringConverter::StringToWstring(message) + L"\n" + error.ErrorMessage();
MessageBoxW(NULL, error_message.c_str(), L"Error", MB_ICONERROR);
}

static void Log(HRESULT hr, std::wstring message) {...略}
static void Log(COMException & exception)
{
std::wstring error_message = exception.what();
MessageBoxW(NULL, error_message.c_str(), L"Error", MB_ICONERROR);
}
};

好了,终于可以回到正文,为了渲染出对象我们还要做很多工作...
渲染目标视图
渲染目标视图是D3D11中的一种资源视图,类似于原始内存块。资源视图允许资源在特定阶段绑定到图形管道,将交换链的后缓冲区绑定为渲染目标这使D3D11能够渲染到它上面
// 接上个函数
Microsoft::WRL::ComPtr<ID3D11Texture2D> backBuffer; // 2DTexture类型
hr = this->swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())); // 在交换链上找到后台缓冲区并使用它来创建backBuffer纹理对象
COM_ERROR_IF_FAILED(hr, "GetBuffer Failed.");
// 创建 渲染目标视图
hr = this->device->CreateRenderTargetView(backBuffer.Get(), NULL, this->renderTargetView.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create render target view.");
...

深度模板
创建深度/模板缓冲区用于深度测试
// Depth/Stencil Buffer
CD3D11_TEXTURE2D_DESC depthStencilTextureDesc(DXGI_FORMAT_D24_UNORM_S8_UINT, this->windowWidth, this->windowHeight); // 描述方式
depthStencilTextureDesc.MipLevels = 1;
depthStencilTextureDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
hr = this->device->CreateTexture2D(&depthStencilTextureDesc, NULL, this->depthStencilBuffer.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create depth stencil buffer.");
hr = this->device->CreateDepthStencilView(this->depthStencilBuffer.Get(), NULL, this->depthStencilView.GetAddressOf()); // 将创建好的2D纹理绑定到新建的深度/模板视图
COM_ERROR_IF_FAILED(hr, "Failed to create depth stencil view.");
this->deviceContext->OMSetRenderTargets(1, this->renderTargetView.GetAddressOf(), this->depthStencilView.Get()); // 输出合并阶段 绑定渲染目标视图和深度/模板视图

// Create depth stencil state
CD3D11_DEPTH_STENCIL_DESC depthstencildesc(D3D11_DEFAULT);
depthstencildesc.DepthEnable = true;
depthstencildesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
depthstencildesc.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;
hr = this->device->CreateDepthStencilState(&depthstencildesc, this->depthStencilState.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create depth stencil state.");

视口
最终我们还需要决定将整个视图输出到窗口特定的范围。因此我们需要使用D3D11_VIEWPORT来设置视口:
// Create & set the Viewport
CD3D11_VIEWPORT viewport(0.0f, 0.0f, static_cast<float>(this->windowWidth), static_cast<float>(this->windowHeight)); // 设置初始参数
this->deviceContext->RSSetViewports(1, &viewport);

光栅化
我们设置了只渲染正面
// Create Rasterizer State
CD3D11_RASTERIZER_DESC rasterizerDesc(D3D11_DEFAULT);
hr = this->device->CreateRasterizerState(&rasterizerDesc, this->rasterizerState.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create rasterizer state.");

// Create Rasterizer State for culling front
CD3D11_RASTERIZER_DESC rasterizerDesc_CullFront(D3D11_DEFAULT);
rasterizerDesc_CullFront.CullMode = D3D11_CULL_MODE::D3D11_CULL_FRONT;
hr = this->device->CreateRasterizerState(&rasterizerDesc_CullFront, this->rasterizerState_CullFront.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create rasterizer state.");

混合
混合半透设置
// Create Blend State
D3D11_RENDER_TARGET_BLEND_DESC rtbd = { 0 };
rtbd.BlendEnable = true;
rtbd.SrcBlend = D3D11_BLEND::D3D11_BLEND_SRC_ALPHA;
rtbd.DestBlend = D3D11_BLEND::D3D11_BLEND_INV_SRC_ALPHA;
rtbd.BlendOp = D3D11_BLEND_OP::D3D11_BLEND_OP_ADD;
rtbd.SrcBlendAlpha = D3D11_BLEND::D3D11_BLEND_ONE;
rtbd.DestBlendAlpha = D3D11_BLEND::D3D11_BLEND_ZERO;
rtbd.BlendOpAlpha = D3D11_BLEND_OP::D3D11_BLEND_OP_ADD;
rtbd.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE::D3D11_COLOR_WRITE_ENABLE_ALL;
D3D11_BLEND_DESC blendDesc = { 0 };
blendDesc.RenderTarget[0] = rtbd;
hr = this->device->CreateBlendState(&blendDesc, this->blendState.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create blend state.");

最后,(贴图)采样器
spriteBatch = std::make_unique<DirectX::SpriteBatch>(this->deviceContext.Get());
spriteFont = std::make_unique<DirectX::SpriteFont>(this->device.Get(), L"Data\\Fonts\\comic_sans_ms_16.spritefont");

// Create sampler description for sampler state
CD3D11_SAMPLER_DESC sampDesc(D3D11_DEFAULT);
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
hr = this->device->CreateSamplerState(&sampDesc, this->samplerState.GetAddressOf());
COM_ERROR_IF_FAILED(hr, "Failed to create sampler state.");
}
catch (COMException & exception) // 异常处理
{
ErrorLogger::Log(exception);
return false;
}
return true;
}

终于,可以渲染帧
void Render()
{
// Just clear the backbuffer
g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, Colors::MidnightBlue ); // 将后台缓冲区清除为蓝色
g_pSwapChain->Present( 0, 0 ); // 切换后台缓冲区和前台缓冲区
}
这时运行我们就能看到一个被渲染成蓝色的窗口了

其他(知识补充)

基元
基元是3D环境中的单个元素,可以是三角形,直线,点等等。以下是可以组合基元以创建3D对象的方法列表。
  1. 点列表
  2. 线列表
  3. 线条
  4. 三角形列表
  5. 三角形条带

鼠标和键盘输入
一次输入封装成一个Event
再由Mgr记录和控制多次输入的Event
[MouseEvent.h]
struct MousePoint
{
int x;
int y;
};

[KeyboardEvent.h]
class KeyboardEvent
{
public:
enum EventType
{
Press,
Release,
Invalid
};
KeyboardEvent();
KeyboardEvent(const EventType type, const unsigned char key);
bool IsPress() const;
bool IsRelease() const;
bool IsValid() const;
unsigned char GetKeycode() const;
private:
EventType type;
unsigned char key;
};

[Keyboard.h]
#include <queue>

class Keyboard
{
public:
Keyboard();
bool KeyIsPressed(const unsigned char keycode);
bool KeyBufferIsEmpty();
bool CharBufferIsEmpty();
KeyboardEvent ReadKey();
unsigned char ReadChar();
void OnKeyPressed(const unsigned char key)
{
this->keyStates[key] = true;
this->keyBuffer.push(KeyboardEvent(KeyboardEvent::EventType::Press, key));
}
void OnKeyReleased(const unsigned char key);
void OnChar(const unsigned char key);
void EnableAutoRepeatKeys();
void DisableAutoRepeatKeys();
void EnableAutoRepeatChars();
void DisableAutoRepeatChars();
bool IsKeysAutoRepeat();
bool IsCharsAutoRepeat();
private:
bool autoRepeatKeys = false;
bool autoRepeatChars = false;
bool keyStates[256];
std::queue<KeyboardEvent> keyBuffer;
std::queue<unsigned char> charBuffer;
};
这些类实例放到Engine中进行处理,发挥作用

Timer计时器
基于chrono的now、duration等接口实现
#include <chrono>
class Timer
{
public:
Timer();
double GetElapsedTime();
void Restart();
bool Stop();
bool Start();
private:
bool isRunning = false;
#ifdef _WIN32
std::chrono::time_point<std::chrono::steady_clock> start;
std::chrono::time_point<std::chrono::steady_clock> stop;
#else
std::chrono::time_point<std::chrono::system_clock> start;
std::chrono::time_point<std::chrono::system_clock> stop;
#endif
};

COMException
是我们封装的一个异常处理模块
#define COM_ERROR_IF_FAILED( hr, msg ) if( FAILED( hr ) ) throw COMException( hr, msg, __FILE__, __FUNCTION__, __LINE__ )
class COMException
{
public:
COMException(HRESULT hr, const std::string& msg, const std::string& file, const std::string& function, int line)
{
_com_error error(hr);
whatmsg = L"Msg: " + StringConverter::StringToWstring(std::string(msg)) + L"\n";
whatmsg += error.ErrorMessage();
whatmsg += L"\nFile: " + StringConverter::StringToWstring(file);
whatmsg += L"\nFunction: " + StringConverter::StringToWstring(function);
whatmsg += L"\nLine: " + StringConverter::StringToWstring(std::to_string(line));
}
const wchar_t * what() const
{
return whatmsg.c_str();
}
private:
std::wstring whatmsg;
};



comments powered by Disqus