d3d12龙书学习之MiniEngine的最小化实现(一)
文章目录
MiniEngine介绍
MiniEngine是Microsoft的一个dx12的核心实现,项目地址:https://github.com/Microsoft/DirectX-Graphics-Samples
可以在MiniEngine的基础上直接开发游戏,MiniEngine实现了很多常用功能:
项目目的
对于魔力高清单机版来说,选一个简单的引擎是很有必要的。
初步计划的是tiled map地图、3d角色。后期再把地图物件换成3d。
简单调研了几个商业引擎,感觉总有各种各样的不满意。学习成本非常高,所以准备自己实现一套很简单的引擎,只做当前项目需要的功能。
在实现引擎之前,需要打好基础,也就是学习d3d12的龙书。
龙书中的代码结构不太喜欢,这时候看微软的dx12例子,发现了MiniEngine引擎,觉得非常棒。
但学习MiniEngine也比较麻烦,文件非常多。于是准备做拆解。
通过龙书学习,每次只使用MiniEngine种最少的文件来实现当前章节功能,添加中文注释。
相信随着龙书的学习,可以慢慢地把MiniEngine的功能回复回来。也就可以做到吃透MiniEngine代码。
本次学习的项目地址:https://github.com/mversace/DirectX12-MiniEngine-Dragon
前期准备
- 本项目采用的是最新的visual studio 2019
- 下载微软官方的MiniEngine源码:https://github.com/Microsoft/DirectX-Graphics-Samples/tree/master/MiniEngine
- 下载d3d12龙书的源码:https://github.com/d3dcoder/d3d12book
龙书第四章的实现过程
这一章节主要讲的是dx12的初始化。
根据教材本次项目分为如下步骤:
- 创建ID3D12Device接口实例
- 创建ID3D12Fence对象,并查询描述符大小
- 检测4x MSAA的支持(这一步骤没有必要性)
- 创建命令队列、命令列表分配器和主命令列表
- 创建交换链
- 创建描述符堆
- 调整后台缓冲区大小,创建渲染目标视图
- 创建深度/模板缓冲区以及视图
- 设置视口、裁剪矩形
第一次拆解工程,讲的会比较详细,之后的博客对于拆解过程就简单略过。
实际上在此之前,需要先创建一个win32窗口,并做好消息循环。这是很基础的东西,不深入讨论,简单介绍下:
0. 创建win32窗口
先看下MiniEngine的实现,该项目的启动工程在ModelViewer种的ModelViewer_VS15.sln
打开之后在开始找入口函数:
很容易发现是采用一个宏定义包起来的
CREATE_APPLICATION( ModelViewer )
在GameCore.h中
#define CREATE_APPLICATION( app_class ) \
MAIN_FUNCTION() \
{ \
IGameApp* app = new app_class(); \
GameCore::RunApplication( *app, L#app_class ); \
delete app; \
return 0; \
}
我不喜欢这种宏定义的方式,自己新建立一个空的win32项目
添加上几个文件,简单的实现一个类继承IGameApp
#pragma once
#include "GameCore.h"
class GameApp : public GameCore::IGameApp
{
public:
GameApp(void) {}
virtual void Startup(void) override;
virtual void Cleanup(void) override;
virtual void Update(float deltaT) override;
virtual void RenderScene(void) override;
};
工程目录如下:
然后把GameCore中跟创建窗口无关的代码全部删除,删掉跨平台的宏定义等,只保留很基本的几个函数。
代码删得越多,阅读起来越简单。
此时发现GameCore中好汉了一个pch.h文件,这个文件就是预编译头,把pch.h和pch.cpp放入工程中,删掉没有的头文件:
注意到上边包含有d3dx12.h这个文件是一个dx12的封装文件,微软提供的,从Mini Engine工程中拷进来。
此时工程如下:
到这一步的代码已经提交到了github上,以做记录:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/f73c7f103376c4a47630dd00bb1e7fc72a03c346
即便是删除了很多代码,工程依旧是编译不过去掉。比如提示缺少一些宏定义
通过查看原先工程,发现是Unity.h中的,这个文件简单阅读下,是通用的一些函数,可以直接添加进来
Unity.cpp中调用了Math库。Math简单阅读下,就是一个数学封装库。这些文件是很通用的,不涉及到渲染逻辑的东西。可以一股脑添加进来。
注意添加进来后,给pch.h添加这俩头文件
此时编译,发现还有一个错误‘error C3861: “_BitScanReverse64”’
这个只要把工程改成64位的就可以了。
好了可以编译过了,可以发现已经可以启动一个win32窗口了。
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/39e9bb9bb812f23170ab8a2f1516f9a89c1c86b9
1. 创建ID3D12Device接口实例
其实在刚才删代码的过程中,可以看到MiniEngine的dx12初始化在Graphics的命名空间中。
这里怎么拆解呢。先看下d3d12龙书代码中的初始化过程
先通过CreateDXGIFactory1请求一个接口,然后枚举当前机器的显卡,之后选择一个显卡初始化对应的device
那么我们去MiniEngine工程中搜索‘CreateDXGIFactory1’发现没有,实际上这里面用的是‘CreateDXGIFactory2’
dx12中随着版本的不同,这种api还是有一些的,所以搜索函数时不要带着后边的数字。
好了,到这里发现是在‘Graphics::Initialize’初始化的,把GraphicsCore俩文件拷进工程。
开始删代码。
删除了一大堆‘没用的’代码之后,记得在GameCore中添加回对于Graphics中函数的调用(自行通过对比文件添加)
以及GameCore中添加对应的头文件。
编译下,打上断点,发现过了
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/c4700af5497c85b68bd0f07ebbf322cfe9542b63
2. 创建ID3D12Fence对象,并查询描述符大小
同样的查找’CreateFence’,发现这个文件东西很多。具体分析过程就不列出了。
对于命令队列等整理如下:
接口说明:
--命令队列:ID3D12CommandQueue
--命令列表:ID3D12CommandList
--命令分配器:ID3D12CommandAllocator
--围栏:ID3D12Fence
文件说明:
--CommandAllocatorPool
--命令分配器池,需要初始化为一种特定类型。通过围栏机制可以做到分配器复用
--CommandListManager
--维护命令队列、命令列表、围栏。
控制GPU执行命令流程:
1. 假设已经有了ID3D12Device
2. 生成一个围栏ID3D12Fence
ID3D12Device->CreateFence
3. 创建针对该设备的命令队列: ID3D12CommandQueue
ID3D12Device->CreateCommandQueue
4. 创建一个命令分配器:ID3D12CommandAllocator(对应你所要执行的命令类型)
ID3D12Device->CreateCommandAllocator
5. 使用该命令分配器生成一个命令列表: ID3D12CommandList
ID3D12Device->CreateCommandList
6. 向命令列表中插入命令
ID3D12CommandList->xxx // 插入命令
ID3D12CommandList->xxx // 插入命令
CreateCommandList->close(); // 关闭
7. 发送给GPU执行命令
ID3D12CommandQueue->ExecuteCommandLists
8. 插入围栏值
ID3D12CommandQueue-Signal
9. 其他操作,交换缓冲区等,不属于这里的功能
说明如下:
1. 步骤3、4、5是必备的几个东西。
2. 步骤6,实际是把命令插入了命令分配器中
3. 步骤7,仅仅是告诉GPU开始执行,GPU会读取命令分配器中的命令逐条执行
4. 步骤8,因为GPU维护的是一个队列(环形队列),只有在执行完上边的命令后才会执行到这个围栏
执行到这个围栏时,会把这里设置的围栏值更新到围栏对象中,使得围栏对象可以知道步骤7的命令是否执行完
为了容易区分功能,添加对应的4个文件,工程目录如下:
这几个文件相对于MiniEngine来说没做修改,仅仅添加了中文注释以及说明文档
在相应的文件中加上这几个文件的调用。
顺路再给GraphicsCore中补上几个窗口状态的函数。
实际上这一步把step4也做完了。
在Graphics::Present中发现了systemtime的函数,这个文件可以加入工程,通用功能,对应的垂直同步变量一起加进来。
对应的BoolVar的头文件加进来,暂时删除掉‘没用的’代码。
EngineTuning实际上是把常用变量封装了一下,通用代码。但里面的一些输出暂时用不上。
注意添加对应文件的头文件。
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/beab7dda54ac5b875da047e24388498d531b3c4f
3. 检测4x MSAA的支持(这一步骤没有必要性)
没有必要性,不做
4. 创建命令队列、命令列表分配器和主命令列表
在step2已经做了
5. 创建交换链
搜索‘CreateSwapChainForHwnd’
添加对应的变量,这一步比较简单
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/b278324a01525afbf94c161154bf77f932d653c3
6. 创建描述符堆
搜索‘CreateDescriptorHeap’
这给描述堆的调用是通过DescriptorAllocator g_DescriptorAllocator[];来做的
添加对应文件,因为‘UserDescriptorHeap’堆,我们在龙书第四章是用不到的,暂时注释掉
对应github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/04a72a95ded69c5c0c2e213d3ad3b98dec1dc9f2
7. 调整后台缓冲区大小,创建渲染目标视图
这里需要很仔细的分析文件,慢慢的拆解。过程略
MiniEngine的缓冲区分析如下:
接口说明:
--资源:ID3D12Resource
--描述符句柄:D3D12_CPU_DESCRIPTOR_HANDLE
文件说明:
--EsramAllocator
--无用类,也许是微软暂时还没有实现
--GpuResource
--对ID3D12Resource的简单封装
--PixelBuffer -> GpuResource
--像素缓冲区
--对于资源来说,很多就是gpu中的一块内存,可以叫buff、缓冲区等
--这里实现的就是像素缓冲区,规定该buff个结构是像素类型,规定了每个像素的格式
--ColorBuffer -> PixelBuffer
--颜色缓冲区
--进一步规定了每个像素的结构是颜色格式
--还维护有3种描述符句柄:
----m_SRVHandle: 着色器资源视图句柄
----m_RTVHandle: 渲染目标视图 句柄 !!通过Create创建的缓冲区才会创建该视图
----m_UAVHandle[12]: 无序访问视图句柄 !!通过Create创建的缓冲区才会创建该视图
--DepthBuffer -> PixelBuffer
--深度/模板缓冲区
--维护有3种描述符句柄:
----m_hDSV[4]: 4种不同意义的深度视图句柄
----m_hDepthSRV: 深度着色器资源视图句柄
----m_hStencilSRV: 模板着色器资源视图句柄
--CommandContext
--对命令的整体封装,方便使用
此时我们的工程结构如下:
随着对MiniEngine了解的加深。这里还原了一些文件,并采用注释的方法,这样更方便以后改回来;
本次github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/6bd1ce05b976bb309847344ade115bdd508d16be
8. 创建深度/模板缓冲区以及视图
因为step已经把深度缓冲区放入了工程内,在MiniEngine中搜索下对应的调用在哪里。
发现是在BufferManager中
这个文件中包含了很多的缓冲区类型,简单删除一下,只保留需要的深度缓冲区
我们查下原先是在哪里调用这个东西,发现是在‘InitializeRenderingBuffers’,查下所有引用
发现在初始化,以及分辨率改变后调用。因为EngineTuning我还没有研究,所以我们简单的在resize事件中调用即可。因为窗口创建后也会进入resize函数一次。
9. 设置视口、裁剪矩形
这个实际已经包含在step7了
10.参照d3d12book中第四章的内容编写代码。
如果是一路自己分析下来的,对这些简单的接口应该有足够印象里,直接编写对应的代码
void GameApp::RenderScene(void)
{
GraphicsContext& gfxContext = GraphicsContext::Begin(L"Scene Render");
gfxContext.TransitionResource(g_DisplayPlane[g_CurrentBuffer], D3D12_RESOURCE_STATE_RENDER_TARGET);
gfxContext.SetViewportAndScissor(0, 0, g_DisplayWidth, g_DisplayHeight);
g_DisplayPlane[g_CurrentBuffer].SetClearColor({ 0.690196097f, 0.768627524f, 0.870588303f, 1.000000000f });
gfxContext.ClearColor(g_DisplayPlane[g_CurrentBuffer]);
gfxContext.TransitionResource(g_SceneDepthBuffer, D3D12_RESOURCE_STATE_DEPTH_WRITE, true);
gfxContext.ClearDepth(g_SceneDepthBuffer);
gfxContext.TransitionResource(g_SceneDepthBuffer, D3D12_RESOURCE_STATE_DEPTH_READ);
gfxContext.SetRenderTarget(g_DisplayPlane[g_CurrentBuffer].GetRTV(), g_SceneDepthBuffer.GetDSV_DepthReadOnly());
gfxContext.TransitionResource(g_DisplayPlane[g_CurrentBuffer], D3D12_RESOURCE_STATE_PRESENT);
gfxContext.Finish();
}
运行:
本项目结束。第四章的工程不在修改。直接看github就好了
接下来的几章,会直接以本章为基础,循序渐进,慢慢的回填所需要的功能。