前言
上一篇文章其实挺杂乱的,最主要还是学习了一些可以用到的工程技巧吧。从稍微纵观的方向来看,本质上写的东西就是:建立了一个控制整个渲染引擎的Application/应用类,然后他目前可以在初始化阶段建立窗口。 除此之外加了个Debug用的Log系统。虽然感觉工作量不是很多,但在工程上和结构上其实还是对我提供了不少开导吧。
作为人生的第一个渲染玩具,也不想考虑太多什么【可兼容多个渲染API】类似的啥的操作,目前也不会往弄成游戏引擎方向想。
目前来说基本上是这种感觉:
1
2
3
4
5
6
7
8
9
int main()
{
LogSystem::Init();
Application* App = new Application();
App->Init();
App->Run();
delete App;
return;
}
这里的话要先确保Application类本身的流程可以正常,之后再考虑通过继承来覆写部分初始化函数来比如加载场景。
这一篇简单的梳理一下整个引擎运行的一个流程,对Application类进行进一步的更新,也就是决定在Initialization和Run阶段都要负责做什么。大部分都是参考之前写过的文章:DirectX12 初始化与渲染流程
本篇文章以绘制出窗口为目的,后期的整体流程肯定会更复杂
DX12封装思考
整体结构
在开始讨论整体流程先,先要决定怎么去封装整个DX12。
在Github上的很多开源引擎里,一般会有一个不限API的Renderer中间层,然后再通过中间层去调用这个DX12。这次因为要做的是一个纯DX12的渲染玩具,就不放太多心思在这一部分中间层的包装了。所以我需要一个负责从初始化到渲染都用来管理DX12的一个管理类,我叫他DX12Renderer。
简单来说就是,Application类能接触到的DX12相关的任何操作,都只需要通过这个单一的管理类。之后在Application类内的话代码就会变成下面这种感觉:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Application::Foo()
{
DX12Renderer& renderer = DX12Renderer::GetInstance();
// Init
renderer->InitDX12();
renderer->InitPipeline();
renderer->LoadScene();
// Update
renderer->Update();
// Render
renderer->PrepareCmdList();
renderer->Draw();
renderer->CloseCmdList();
}
DX12内部封装
DX12相较于龙书的构造来说,其实有特别多可以包装的东西。基本上见到的比如说Buffer,Texture,Shader什么的,或者说根签名和根参数等,全部都是可以进行包装到另外的类里进行管理。进行封装的话可以在我们有大量的不同管线的情况(大量PSO,Shader,Root Signature等)可以更简单的对他们进行管理。
Mesh
这里把Mesh单独拉出来说主要还是因为这东西在现阶段来看是比较独特的。我需要在渲染器外面去添加、定义物体,也就是说我需要在Application侧去首先加载用到的模型和物体,并且运行时在Application内去调整他们的位置的变化等。但同时我们又需要他的数据来在渲染器内部进行渲染,所以这里的抽象还是比较难的。
初始化
系统初始化
首先初始化这里我们主要考虑这几个部分:
- 窗口初始化
- 消息系统初始化
- DX12初始化
- 其他工具初始化(例如Imgui)
窗口初始化
Win32窗口本身的建立其实基本上大同小异,这里直接用龙书里面的代码就可以了,不过消息这里需要稍后进行处理。因为我们把窗口本身跟Application类隔开了,这里比起在MsgProc() 里决定对每种消息的反应,更应该是把消息全部提交回应用让应用类决定。
消息系统初始化
消息系统这里相对其他来说倒是不需要特别的去建立一个管理类。所谓消息系统初始化其实就是窗口类中设置好CallBack(类似委托),然后在收到任何消息的时候都会去调用Application类里面的OnEvent()函数去进行一个处理。
DX12初始化
DX12初始化的实际上分为两部分,一部分是系统本身的初始化,例如建立D3dDevice,以及绑定到窗口等。 在这之外我们还需要考虑对整个管线的初始化,也就是加载Shader等操作。 不过以建立窗口为目标的话目前还是不需要处理管线相关的事情。
最基础的初始化是这样子的。
1
2
3
4
5
6
7
8
9
10
11
12
13
void InitDX12()
{
// 建立工厂
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
// 建立device
D3D12CreateDevice(nullptr,D3D_FEATURE_LEVEL_11_0,IID_PPV_ARGS(&md3dDevice));
// 建立同步用的fence
md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,IID_PPV_ARGS(&mFence)));
// 建立(cmdList, cmdListAlloc, cmdQueue)
CreateCommandObjects();
// 建立SwapChain(两个交替的后缓冲)
CreateSwapChain();
}
当然这里进行初始化之后我们还是没有办法去用一个Clear color来绘制一个底色,因为我们只初始化了DX12的系统,但并没有为交换链绑定任何的RTV。就是说我们还没有地方可以渲染出一个能放到后缓冲上的贴图。
我们如果要绘制一个底色可以直接给这个DX12添加一个大小为SwapChain数量的RTV Heap,和一个默认的DSV,然后把RTV绑定给我们的交换链就可以绘制出清除色了。
更新
窗口更新
在上面设置好RTV后我们只需要进行下面的操作就可以绘制一个简单的Clear color:
1
2
3
4
5
6
7
8
9
10
11
cmdListAlloc->Reset();
mCmdList->Reset(cmdListAlloc.Get(), nullptr);
mCmdList->RSSetViewports(1, &mScreenViewport);
mCmdList->RSSetScissorRects(1, &mScissorRect);
mCmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
mCmdList->ClearRenderTargetView(CurrentBackBufferView(), DirectX::Colors::LightSteelBlue, 0, nullptr);
mCmdList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
mCmdList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
然后再提交指令到GPU就可以了
IMGUI的加入
考虑到一个引擎的灵活性,这里采用了imgui当GUI子系统。这里添加了一个Imgui的管理类来负责管理这个子系统的行为。
初始化
Imgui的初始化是要在窗口以及DX12后进行的,因为他会用到两者的接口。并且因为实际上我是在使用DX12来绘制这个GUI,所以在初始化DX12的时候需要额外建立一个Shader resource view提供给Imgui使用。
1
2
3
4
5
ImGui_ImplWin32_Init(App.GetWindow()->MainWnd());
ImGui_ImplDX12_Init(DXContext.D3dDevice().Get(), NUM_FRAMES_IN_FLIGHT,
DXGI_FORMAT_R8G8B8A8_UNORM, DXContext.SrvHeap().Get(),
DXContext.SrvHeap()->GetCPUDescriptorHandleForHeapStart(),
DXContext.SrvHeap()->GetGPUDescriptorHandleForHeapStart());
绘制
Imgui作为一个GUI肯定是最后进行绘制的,也就是在场景渲染完后才会调用他的绘制。这里需要注意的是添加Imgui后不能在常规的绘制后关闭掉我们的CommandList。所有的跟绘制有关的操作都是先提交到CommandList里然后之后再一同传入GPU的。所以比起常规的渲染出物体后就提交的写法,这里要让图形API把关闭渲染器作为一个函数独立出来,这样我们在应用测得绘制会变成下面这种样子:
1
2
3
4
5
6
void Draw()
{
renderer->Draw();
ImguiManager::ImguiDraw();
renderer->CloseCmdList();
}