前几天突然肚子疼,晚上疼的睡不着,医院一日游花了千把检查费,配了点消炎药,现在好多了。所以我们干IT的真的要好好保护身体,攒够革命的本钱,别给医院打工了。好了,废话不多说,今天我们接上一篇博客的代码,继续初始化。
我们对之前的代码略微做点封装,但是为了利于逻辑的理解,不会一下子封装的很深。首先,我们建一个InitWindow函数,然后将之前写的创建窗口代码拷贝进去。此函数返回一个布尔值,如果窗口创建成功则返回true。
bool InitWindow(HINSTANCE hInstance, int nShowCmd)
{
//窗口初始化描述结构体(WNDCLASS)
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW; //当工作区宽高改变,则重新绘制窗口
wc.lpfnWndProc = MainWndProc; //指定窗口过程
wc.cbClsExtra = 0; //借助这两个字段来为当前应用分配额外的内存空间(这里不分配,所以置0)
wc.cbWndExtra = 0; //借助这两个字段来为当前应用分配额外的内存空间(这里不分配,所以置0)
wc.hInstance = hInstance; //应用程序实例句柄(由WinMain传入)
wc.hIcon = LoadIcon(0, IDC_ARROW); //使用默认的应用程序图标
wc.hCursor = LoadCursor(0, IDC_ARROW); //使用标准的鼠标指针样式
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //指定了白色背景画刷句柄
wc.lpszMenuName = 0; //没有菜单栏
wc.lpszClassName = L"MainWnd"; //窗口名
//窗口类注册失败
if (!RegisterClass(&wc))
{
//消息框函数,参数1:消息框所属窗口句柄,可为NULL。参数2:消息框显示的文本信息。参数3:标题文本。参数4:消息框样式
MessageBox(0, L"RegisterClass Failed", 0, 0);
return 0;
}
//窗口类注册成功
RECT R; //裁剪矩形
R.left = 0;
R.top = 0;
R.right = 1280;
R.bottom = 720;
AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false); //根据窗口的客户区大小计算窗口的大小
int width = R.right - R.left;
int hight = R.bottom - R.top;
//创建窗口,返回布尔值
//CreateWindowW(lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)
mhMainWnd = CreateWindow(L"MainWnd", L"DX12Initialize", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, hight, 0, 0, hInstance, 0);
//窗口创建失败
if (!mhMainWnd)
{
MessageBox(0, L"CreatWindow Failed", 0, 0);
return 0;
}
//窗口创建成功,则显示并更新窗口
ShowWindow(mhMainWnd, nShowCmd);
UpdateWindow(mhMainWnd);
return true;
}
然后我们新建一个Run函数,将之前的消息循环代码复制进去,并将其结构略作调整。因为考虑到我们是游戏程序,所以调整了分支结构,如果没有消息处理,就执行游戏画面和逻辑的计算,这里的Draw函数之后会定义(Draw函数每运行一次,其实就是一帧,所以后期会在它前面加上帧时间的计算)。
int Run()
{
//消息循环
//定义消息结构体
MSG msg = { 0 };
//如果GetMessage函数不等于0,说明没有接受到WM_QUIT
while (msg.message != WM_QUIT)
{
//如果有窗口消息就进行处理
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) //PeekMessage函数会自动填充msg结构体元素
{
TranslateMessage(&msg); //键盘按键转换,将虚拟键消息转换为字符消息
DispatchMessage(&msg); //把消息分派给相应的窗口过程
}
//否则就执行动画和游戏逻辑
else
{
Draw();
}
}
return (int)msg.wParam;
}
接下来我们建一个InitDirect3D函数,将之前我们列出的初始化步骤,除了最后两条(这两条在Draw函数中执行),都拷贝至函数中(这里对之前的步骤都做了函数封装)。我们再来回顾下,做了哪些事:
- 开启D3D12调试层。
- 创建设备。
- 创建围栏,同步CPU和GPU。
- 获取描述符大小。
- 设置MSAA抗锯齿属性。
- 创建命令队列、命令列表、命令分配器。
- 创建交换链。
- 创建描述符堆。
- 创建描述符。
- 资源转换。
- 设置视口和裁剪矩形。
注意:由于封装了函数,我们将一些公用的ComPtr和结构体以及变量都提到了外面。
//声明指针接口和变量
ComPtr<ID3D12Device> d3dDevice;
ComPtr<IDXGIFactory4> dxgiFactory;
ComPtr<ID3D12Fence> fence;
ComPtr<ID3D12CommandAllocator> cmdAllocator;
ComPtr<ID3D12CommandQueue> cmdQueue;
ComPtr<ID3D12GraphicsCommandList> cmdList;
ComPtr<ID3D12Resource> depthStencilBuffer;
ComPtr<ID3D12Resource> swapChainBuffer[2];
ComPtr<IDXGISwapChain> swapChain;
ComPtr<ID3D12DescriptorHeap> rtvHeap;
ComPtr<ID3D12DescriptorHeap> dsvHeap;
D3D12_VIEWPORT viewPort;
D3D12_RECT scissorRect;
UINT rtvDescriptorSize = 0;
UINT dsvDescriptorSize = 0;
UINT cbv_srv_uavDescriptorSize = 0;
UINT mCurrentBackBuffer = 0;
bool InitDirect3D()
{
/*开启D3D12调试层*/
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
CreateDevice();
CreateFence();
GetDescriptorSize();
SetMSAA();
CreateCommandObject();
CreateSwapChain();
CreateDescriptorHeap();
CreateRTV();
CreateDSV();
CreateViewPortAndScissorRect();
return true;
}
接下来我们建一个总的Init函数,将InitWindow和InitDirect3D放一起做一个判断,如果两个初始化都正常执行,则判断总的初始化正常执行,返回true。
bool Init(HINSTANCE hInstance, int nShowCmd)
{
if (!InitWindow(hInstance, nShowCmd))
{
return false;
}
else if (!InitDirect3D())
{
return false;
}
else
{
return true;
}
}
然后我们修改WinMain函数,因为要处理抛出异常,所以我们使用try-catch结构。如果初始化成功,则执行Run函数,并通过DxException类捕获异常,返回异常的函数名,以及所在行号。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int nShowCmd)
{
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
try
{
if ( ! Init(hInstance, nShowCmd) )
return 0;
return Run();
}
catch (DxException& e)
{
MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
return 0;
}
}
大致的代码结构就是这样,现在我们来处理刚提到的Draw函数,这是今天的重点。
void Draw();
Draw函数主要是将我们的各种资源设置到渲染流水线上,并最终发出绘制命令。首先重置命令分配器cmdAllocator和命令列表cmdList,目的是重置命令和列表,复用相关内存。
ThrowIfFailed(cmdAllocator->Reset());//重复使用记录命令的相关内存
ThrowIfFailed(cmdList->Reset(cmdAllocator.Get(), nullptr));//复用命令列表及其内存
接着我们将后台缓冲资源从呈现状态转换到渲染目标状态(即准备接收图像渲染)。
UINT& ref_mCurrentBackBuffer = mCurrentBackBuffer;
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(swapChainBuffer[ref_mCurrentBackBuffer].Get(),//转换资源为后台缓冲区资源
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));//从呈现到渲染目标转换
接下来设置视口和裁剪矩形。
cmdList->RSSetViewports(1, &viewPort);
cmdList->RSSetScissorRects(1, &scissorRect);
然后清除后台缓冲区和深度缓冲区,并赋值。步骤是先获得堆中描述符句柄(即地址),再通过ClearRenderTargetView函数和ClearDepthStencilView函数做清除和赋值。这里我们将RT资源背景色赋值为DarkRed(暗红)。
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = CD3DX12_CPU_DESCRIPTOR_HANDLE(rtvHeap->GetCPUDescriptorHandleForHeapStart(), ref_mCurrentBackBuffer, rtvDescriptorSize);
cmdList->ClearRenderTargetView(rtvHandle, DirectX::Colors::DarkRed, 0, nullptr);//清除RT背景色为暗红,并且不设置裁剪矩形
D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = dsvHeap->GetCPUDescriptorHandleForHeapStart();
cmdList->ClearDepthStencilView(dsvHandle, //DSV描述符句柄
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, //FLAG
1.0f, //默认深度值
0, //默认模板值
0, //裁剪矩形数量
nullptr); //裁剪矩形指针
然后我们指定将要渲染的缓冲区,即指定RTV和DSV。
cmdList->OMSetRenderTargets(1,//待绑定的RTV数量
&rtvHandle, //指向RTV数组的指针
true, //RTV对象在堆内存中是连续存放的
&dsvHandle); //指向DSV的指针
等到渲染完成,我们要将后台缓冲区的状态改成呈现状态,使其之后推到前台缓冲区显示。完了,关闭命令列表,等待传入命令队列。
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(swapChainBuffer[ref_mCurrentBackBuffer].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));//从渲染目标到呈现
//完成命令的记录关闭命令列表
ThrowIfFailed(cmdList->Close());
等CPU将命令都准备好后,需要将待执行的命令列表加入GPU的命令队列。使用的是ExecuteCommandLists函数。
ID3D12CommandList* commandLists[] = { cmdList.Get() };//声明并定义命令列表数组
cmdQueue->ExecuteCommandLists(_countof(commandLists), commandLists);//将命令从命令列表传至命令队列
然后交换前后台缓冲区索引(这里的算法是1变0,0变1,为了让后台缓冲区索引永远为0)。
ThrowIfFailed(swapChain->Present(0, 0));
ref_mCurrentBackBuffer = (ref_mCurrentBackBuffer + 1) % 2;
最后设置围栏值,刷新命令队列,使CPU和GPU同步,这段代码在第一篇中有详细解释,这里直接封装。
FlushCmdQueue();
至此,第一阶段的初始化代码全部完成,运行结果如下:
可以看到,背景就是我们设置的暗红色。
为了学习理解,我是在一个cpp文件中写的代码,但是考虑到项目后期的使用,还得整理下。所以后面的博客我会重构代码并加入帧的间隔时间,显示FPS,以及每帧更新必要数据。