d3d12龙书学习之MiniEngine的最小化实现(一)

MiniEngine介绍

MiniEngine是Microsoft的一个dx12的核心实现,项目地址:https://github.com/Microsoft/DirectX-Graphics-Samples
可以在MiniEngine的基础上直接开发游戏,MiniEngine实现了很多常用功能:
d3d12龙书学习之MiniEngine的最小化实现(一)

项目目的

对于魔力高清单机版来说,选一个简单的引擎是很有必要的。
初步计划的是tiled map地图、3d角色。后期再把地图物件换成3d。
简单调研了几个商业引擎,感觉总有各种各样的不满意。学习成本非常高,所以准备自己实现一套很简单的引擎,只做当前项目需要的功能。

在实现引擎之前,需要打好基础,也就是学习d3d12的龙书。
龙书中的代码结构不太喜欢,这时候看微软的dx12例子,发现了MiniEngine引擎,觉得非常棒。

但学习MiniEngine也比较麻烦,文件非常多。于是准备做拆解。

通过龙书学习,每次只使用MiniEngine种最少的文件来实现当前章节功能,添加中文注释。
相信随着龙书的学习,可以慢慢地把MiniEngine的功能回复回来。也就可以做到吃透MiniEngine代码。

本次学习的项目地址:https://github.com/mversace/DirectX12-MiniEngine-Dragon

前期准备

  1. 本项目采用的是最新的visual studio 2019
  2. 下载微软官方的MiniEngine源码:https://github.com/Microsoft/DirectX-Graphics-Samples/tree/master/MiniEngine
  3. 下载d3d12龙书的源码:https://github.com/d3dcoder/d3d12book

龙书第四章的实现过程

这一章节主要讲的是dx12的初始化。
根据教材本次项目分为如下步骤:

  1. 创建ID3D12Device接口实例
  2. 创建ID3D12Fence对象,并查询描述符大小
  3. 检测4x MSAA的支持(这一步骤没有必要性)
  4. 创建命令队列、命令列表分配器和主命令列表
  5. 创建交换链
  6. 创建描述符堆
  7. 调整后台缓冲区大小,创建渲染目标视图
  8. 创建深度/模板缓冲区以及视图
  9. 设置视口、裁剪矩形

第一次拆解工程,讲的会比较详细,之后的博客对于拆解过程就简单略过。

实际上在此之前,需要先创建一个win32窗口,并做好消息循环。这是很基础的东西,不深入讨论,简单介绍下:

0. 创建win32窗口

先看下MiniEngine的实现,该项目的启动工程在ModelViewer种的ModelViewer_VS15.sln
打开之后在开始找入口函数:
d3d12龙书学习之MiniEngine的最小化实现(一)
很容易发现是采用一个宏定义包起来的

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;
};

工程目录如下:
d3d12龙书学习之MiniEngine的最小化实现(一)
然后把GameCore中跟创建窗口无关的代码全部删除,删掉跨平台的宏定义等,只保留很基本的几个函数。
代码删得越多,阅读起来越简单。

此时发现GameCore中好汉了一个pch.h文件,这个文件就是预编译头,把pch.h和pch.cpp放入工程中,删掉没有的头文件:
d3d12龙书学习之MiniEngine的最小化实现(一)
注意到上边包含有d3dx12.h这个文件是一个dx12的封装文件,微软提供的,从Mini Engine工程中拷进来。
此时工程如下:
d3d12龙书学习之MiniEngine的最小化实现(一)
到这一步的代码已经提交到了github上,以做记录:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/f73c7f103376c4a47630dd00bb1e7fc72a03c346

即便是删除了很多代码,工程依旧是编译不过去掉。比如提示缺少一些宏定义
d3d12龙书学习之MiniEngine的最小化实现(一)
通过查看原先工程,发现是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个文件,工程目录如下:
d3d12龙书学习之MiniEngine的最小化实现(一)
这几个文件相对于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
--对命令的整体封装,方便使用

此时我们的工程结构如下:
d3d12龙书学习之MiniEngine的最小化实现(一)

随着对MiniEngine了解的加深。这里还原了一些文件,并采用注释的方法,这样更方便以后改回来;
本次github:
https://github.com/mversace/DirectX12-MiniEngine-Dragon/commit/6bd1ce05b976bb309847344ade115bdd508d16be

8. 创建深度/模板缓冲区以及视图

因为step已经把深度缓冲区放入了工程内,在MiniEngine中搜索下对应的调用在哪里。
发现是在BufferManager中
这个文件中包含了很多的缓冲区类型,简单删除一下,只保留需要的深度缓冲区
d3d12龙书学习之MiniEngine的最小化实现(一)
我们查下原先是在哪里调用这个东西,发现是在‘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();
}

运行:
d3d12龙书学习之MiniEngine的最小化实现(一)

本项目结束。第四章的工程不在修改。直接看github就好了

接下来的几章,会直接以本章为基础,循序渐进,慢慢的回填所需要的功能。