渲染引擎开发笔记2

建立DX12窗口

Posted Kongouuu's Blog on November 30, 2021

前言

上一篇文章其实挺杂乱的,最主要还是学习了一些可以用到的工程技巧吧。从稍微纵观的方向来看,本质上写的东西就是:建立了一个控制整个渲染引擎的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类进行进一步的更新,也就是决定在InitializationRun阶段都要负责做什么。大部分都是参考之前写过的文章:DirectX12 初始化与渲染流程

本篇文章以绘制出窗口为目的,后期的整体流程肯定会更复杂

DX12封装思考

整体结构

在开始讨论整体流程先,先要决定怎么去封装整个DX12

Github上的很多开源引擎里,一般会有一个不限APIRenderer中间层,然后再通过中间层去调用这个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相较于龙书的构造来说,其实有特别多可以包装的东西。基本上见到的比如说BufferTextureShader什么的,或者说根签名和根参数等,全部都是可以进行包装到另外的类里进行管理。进行封装的话可以在我们有大量的不同管线的情况(大量PSO,Shader,Root Signature等)可以更简单的对他们进行管理。

Mesh

这里把Mesh单独拉出来说主要还是因为这东西在现阶段来看是比较独特的。我需要在渲染器外面去添加、定义物体,也就是说我需要在Application侧去首先加载用到的模型和物体,并且运行时在Application内去调整他们的位置的变化等。但同时我们又需要他的数据来在渲染器内部进行渲染,所以这里的抽象还是比较难的。

初始化

系统初始化

首先初始化这里我们主要考虑这几个部分:

  1. 窗口初始化
  2. 消息系统初始化
  3. DX12初始化
  4. 其他工具初始化(例如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的加入

考虑到一个引擎的灵活性,这里采用了imguiGUI子系统。这里添加了一个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();
}

结果