Direct3D初始化

预备知识

组件对象模型

组件对象模型**(Component Object Model,COM)**:不受DirectX语言束缚,并且向后兼容的技术

  • 获得COM接口需要借助特定函数,而不是C++的new
  • 删除COM有Release方法,而不是delete
  • Mirrosoft::WRL::ComPtr类是Window是下的**COM**对象的智能指针
  • 当**ComPtr**出作用域时,它会自动调用Release方法
1
2
3
4
5
//Get: 返回一个指向此底层COM接口的指针,此方法常用于把原始COM接口指针作为参数传给函数
ComPtr<ID3D12RootSignature> mRootSignature;
...
//SetGraphicsRootSignature需要获取ID3D12RootSignature*类型的参数
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
1
2
3
4
5
6
7
//GetAddressOf:返回指向此底层COM接口指针的地址,此函数可以利用函数参数返回COM接口的指针
ComPtr<ID3D12CommandAlllocator> mDirectCmdListAlloc;
...
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.GetAddressOf());

1
//Reset:将此ComPtr实例设置为nullptr释放与之相关的所有引用,同时减少COM接口引用次数,此方法功能与将ComPtr目标实例赋值nullptr效果相同
纹理格式

2D纹理是一种由数据元素构成的矩阵,每个元素存储的都是一个像素的颜色

  • DXGI_FORMAT_R32G32B32_FLOAT:每个元素由2个32位无符号整数分量构成,存储的不一定是颜色信息

  • DXGI_FORMAT_R16G16B16A16_UNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,1]**的区间

  • DXGI_FORMAT_R32G32_UINT:每个元素由2个32位无符号整数分量构成

  • DXGI_FORMAT_R8G8B8A8_UNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,1]**的区间

  • DXGI_FORMAT_R8G8B8A8_SNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[-1,1]**的区间

  • DXGI_FORMAT_R9G9B9A9_SINT:每个元素由4个8位无符号分量构成,每个分量都被映射到**[-128,127]**的区间

  • DXGI_FORMAT_R8G8B8A8_UINT:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,255]**的区间

其他格式

  • DXGI_FORMAT_R16G16B16A16_TYPELESS:每个元素由4个16位无符号分量构成,但是没有指出数据类型
交换链和页面翻转

  • 前台缓冲区和后台缓冲区在绘制渲染过程中互换,这种操作称为:呈现(presenting,亦有译作提交、显示)
  • 前后台缓冲区构成了交换链,Direct3D中用==IDXGISwapChain==接口来表示
  • 这个接口不仅储存了前后台缓冲区的纹理,还提供了修改缓冲区大小(IDXGISwapChain::ResiezeBuffers)和呈现缓冲区内容(IDXGISwapChain::Present)的方法
  • 使用2个缓冲区的情况称为**双缓冲(double buffering,亦有译作双重缓冲、双倍缓冲等)**
  • 还可以用更多的缓冲区,使用3个缓冲区就叫作**三重缓冲(triple buffering)**
深度缓冲

深度缓冲区(depth buffer):存储的非图像数据,而是特定像素的深度信息

  • 深度值范围 0.0-1.0
  • 0.0代表观察者在**视锥体**(视域体、视景体、视截体、视体),即观察者能看到的空间范围
  • 1.0代表观察者在视锥体中嫩通过看到的离自己最远的像素
  • 如果后台缓冲区的分辨率位1280x1024,那么深度缓冲去也应当由1280x1024

深度缓冲区的原理:计算每个像素的深度值,并执行**深度测试(depth test)**,具有最小深度值的像素会最终写入后台缓冲

深度缓冲区也是一种纹理,用如下格式来创建

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:占用64位,取其中的32位指定一个浮点型深度缓冲区,另有8位无符号整数分配给*模板缓冲区(stencil buffer)**,并且将该元素映射到[0,255***]
  2. DXGI_FORMAT_D32_FLOAT:指定一个32位浮点型深度缓冲区
  3. DXGI_FORMAT_D24_UNORM_指定一个无符号的24位深度缓冲区,并将该元素映射到[0,1]区间;另有8位无符号整数分配给*模板缓冲区(stencil buffer)**,并且将该元素映射到[0,255***]
  4. DXGI_FORMAT_D16_UNORM:指定一个16位浮点型深度缓冲区,并将该元素映射到[0,1]区间
资源与描述符

资源->中间层(即描述符)->GPU

描述符

  • 一种把送往GPU的资源进行描述的轻量级结构;
  • 绘制所需的资源通过描述符绑定到渲染流水线上;
  • 为GPU解释资源,如告知Direct3D某个资源如何使用(绑定到流水线的哪个阶段);
  • 指定欲绑定资源中的局部数据
常见描述符
  1. CBV/SRV/UAV :分表表示常量缓冲区(constant buffer view)、着色器资源视图(shader resource view)和无序访问试图(unordered access view)3种资源;
  2. 采样器(sampler,亦有译作取样器):表示采样器资源(用于纹理)
  3. RTV:渲染目标视图资源(render target view)
  4. DSV:深度/模板视图资源(depth/stencil view)

描述符堆:存放某种特定类描述符的内存,可以看作是描述符数组

多重采样
超级采样(SSAA)

超级采样:反走样技术

  • 使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲去;
  • 3D场景以这种更大的分辨率渲染到后台缓冲区中;
  • 当数据要从后台缓冲区调往屏幕显示的时候,会将后台缓冲区按4个像素一组进行解析(降采样,downsample),把放大的采样点数降低回原来采样点数每组用求平均值的方法得到相对平滑的像素颜色
  • 实际上是通过软件的方式提升了画面分辨率
  • 超级采样是高开销的操作,因为限速处理数量和占用内存大小都增加到了4倍,因此Direct3D支持一种性能和效果折中的反走样技术:*多重采样(multisampling)**,记作MSAA***
多重采样(MSAA)
  • 多重采样不需要对每个子像素都进行计算
  • 而是仅计算一次像素中心的颜色,在基于可视性和覆盖性将得到的颜色信息分享给其子像素
区别
  • 超级采样:图像颜色要根据每一个像素来计算,因此每个子像素都可以各具不的颜色;开销更大但是更精确

  • 多重采样:每个像素只需要计算一次,最后假尼姑得到的颜色数据复制到多边形覆盖的所有可见子像素中

用Direct3D进行多重采样
1
2
3
4
5
typedef struct DXGI_SAMPLE_DESC
{
UINT count;//指定了每个像素的采样次数,采样次数越多,代价越高
UINT Quality;;//指示用户期望的图像质量级别,不同厂家而言,这个参数相差很多
}

根据给定的纹理格式和采样数量,用ID3D12Device::CheckFeatureSupport方法查询对应的质量级别

  • 考虑到多重采样会占用内存资源,又为了保证程序性能等原因,通常会把采样数量设定位 4 或 8
  • 如果不希望使用多重采样,可以设置采样数量位1,质量设置为0
功能级别

Direct3D 11开始引用了**功能级别(feature level)**,代码里用 D3D_FEATURE_LEVEL表示

1
2
3
4
5
6
7
8
9
10
enum D3D_FEATURE_LEVEL
{
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000,
D3D_FEATURE_LEVEL_11_1 = 0xb100
}
  • 功能级别为不同级别所支持的功能进行严格界定
DirectX图形学基础结构

DirectX图形学基础结构(DXGI)是一种与**Direct3D配合使用的API**

如,IDXGIFactory是DXGI中的关键接口之一,用于创建IDXGISwapChain接口以及枚举显示适配器

一个系统可能由数个显示设备,我们称每一台显示设备都是一个显示输出,用IDXGIOutput接口来表示

功能支持的检测

ID3D12Device::CheckFeatureSupport方法是检测当前图形驱动对多重过采样的支持,圆形如下

1
HRESULT ID3D12Device::CheckFeatureSupport(D3D12_FEATURE Feature,void* pFeatureSuportData,UINT FeatureSupportDataSize);
  • Feature:枚举类型D3D12_FEATURE中的成员之一,用于指定我们希望检测的功能支持类型,具体如下

    • D2D12_FEATURE_D3D12_OPTIONS:检测当前图形驱动对Direct3D 12各种功能的支持情况
    • D3D12_FEATURE_ARCHITECTURE:检测图形适配器中GPU的硬件体系架构特性
    • D3D12_FEATURE_FEATURE_LEVELS:检测对功能级别的支持情况
    • D3D12_FEATURE_FORMAT_SUPPORT:检测对给定纹理格式的支持情况
    • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检测对多重采样功能的支持情况
  • pFeatureSuportData:指向某种数据结构的指针,该结构中存有检索到的特定功能支持的信息,此结构体的具体类型取决于Feature参数

    • D3D12_FEATURE_D3D12_OPTIONS:返回一个D3D12_FEATURE_DATA_D3D12_OPTIONS实例
    • D3D12_FEATURE_ARCHITECTURE:返回D3D12_FEATURE_ARCHITECTURE实例
    • D3D12_FEATURE_FEATURE_LEVELS:同上类推
    • D3D12_FEATURE_FORMAT_SUPPORT:同上类推
    • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:同上类推
  • FeatureSupportDataSize:传回**pFeatureSuportData**参数中的数据结构大小

资源驻留

Direct3D12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况(Direct3D11中则有系统自动管理

一般情况,资源创建时就会驻留在显存中,被销毁时则清出。但是通过下面方法我们可以自己管理资源的驻留

1
2
HRESULT ID3D12Device::MakeResident(UINT NumObjects,ID3D12Pageable* const *ppObjects);
HRESULT ID3D12Device::Evict(UINT NumObjects,ID3D12Pageable* const *ppObjects);

这两个方法的第二个参数都是ID3D12Pageable资源数组,第一个参数表示该数组资源的数量

CPU和GPU的交互

  • 每个GPU都至少维护这一个命令队列(command queue,本质上是环形缓冲区,即ring buffer)。
  • 借助Direct3D API,CPU可以用命令列表(command list)将命令提交到这个队列中去
  • 新加入的命令不会立即执行
  • 假如命令列表空空如也,那么GPU会闲置
  • 假如命令列表填满,那么CPU会在某个时刻保持空闲

在Direct3D 12中,命令队列被抽象为==ID3D12CommandQueue==接口来表示,通过填写D3D12_COMMAND_QUEUE_DESC结构体来表示队列,在通过调用ID3D12Device::CreateCommandQueue方法来创建。

命令队列
创建命令队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//创建队列智能指针
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
...;

D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
//创建队列方法
//IID_PPV_ARGS辅助宏本质是将ppType强制转换为void**类型
//Direct3D 12中创建接口实例的API时,大多数都有一个参数是类型void**的待创接口COM ID
ThrowIfFailed(md3dDevice>CreateCommandQueue(&queueDesc,IID_PPV_ARGS(&mCommandQueue)));

...;
//添加命令到命令列表里
//第一个参数是待执行的命令列表数组的数量
//第二个参数是待执行的命令列表数组
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
//下列两个方法不是执行命令,而是将命令添加到命令列表里,还是需要通过ExecuteCommandLists方法才将命令真正加入到命令列表
mCommandQueue->DrawIndexedInstanced(36,1,0,0,0);
mCommandQueue->RSSetViewports(1,&mScreenViewport);



//结束记录命令
//必须在调用ExecuteCommandLists方法之前先关闭
mCommandQueue->Close();
内存分配器

内存分配器:存储命令列表里的命令,执行ID3D12CommandQueue::ExecuteCommandLists方法时,命令队列就会引用分配器里的命令

1
2
3
4
virtual HRESULT STDMETHODCALLTYPE CreateCommandAllocator( 
_In_ D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
_COM_Outptr_ void **ppCommandAllocator) = 0;
  • type:命令列表类型
    • D3D12_COMMAND_LIST_TYPE_DIRECT:GPU可直接执行的命令
    • D3D12_COMMAND_LIST_TYPE_BUNDLE:打包的命令列表,一般不用
  • riid:适配接口的COM ID
  • ppCommandAllocator:输出指向所建命令分配器的指针
1
2
3
4
5
6
7
8
9
//内存管理指针
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
...;

//第一个参数:此命令分配器相关联的命令列表类型,具体见下图
//第二个参数:内存分配器地址
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
创建命令列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//原型
virtual HRESULT STDMETHODCALLTYPE CreateCommandList(
_In_ UINT nodeMask,
_In_ D3D12_COMMAND_LIST_TYPE type,
_In_ ID3D12CommandAllocator *pCommandAllocator,
_In_opt_ ID3D12PipelineState *pInitialState,
REFIID riid,
_COM_Outptr_ void **ppCommandList) = 0;


//实例
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // Associated command allocator
nullptr, // Initial PipelineStateObject
IID_PPV_ARGS(mCommandList.GetAddressOf())));
  1. nodeMask:如果只有1个GPU,设置成0;多个GPU用于关联的物理GPU
  2. type:命令列表类型
  3. pCommandAllocator:所建命令列表关联的命令分配器,类型必须匹配
  4. pInitialState:指定命令列表的渲染流水线初始状态,一般可以设置为nullptr
  5. riid:待创建的ID3D12CommandList接口的COM ID
  6. ppCommandList:输出指向所建命令列表的指针
小结
  • 可以创建多个关联同一个命令分配器的命令列表
  • 但是不能同时用他们记录命令
  • 其中一个命令列表在记录命令时,必须关闭同一分配器的其他命令列表
  • 要保证命令列表中的所有命令都会按顺序连续的添加到命令分配器内
  • 当创建或重置一个命令列表时,它会处于“**打开** ”状态,所以同时为同一命令列表分配器创建两个命令列表会报错
1
2
3
4
5
6
7
8
9
ID3D12CommandQueue::ExecuteCommandList(C);//把命令添加到命令列表
/*将命令列表恢复到初始状态,借此继续复用其底层内存;
重置列表不会影响命令队列的命令,内存分配器在当中维护
*/
ID3D12GraphicsCommandLIst::Reset();

//GPU提交了一整帧的渲染命令后,我们可能还要为了绘制下一帧服用命令分配器的内存
//注意:在没有确定GPU执行完命令分配器的所有命令之前,千万不要重置命令分配器
ID3D12CommandAllocator::Reset();
GPU与CPU的同步

刷新命令队列:CPU会等待GPU完成所有命令处理,直到到达指定的**围栏点(fence point)**为止。

创建围栏

1
2
3
4
5
6
7
8
HRESULT ID3D12Device::CreateFence(UINT64 InitialValue,
D3D12_FENSE_FLAGS Flags,
REFIID riid,
void** ppFence);
//示例
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void D3DApp::FlushCommandQueue()
{
// 增加围栏值,接下来将命令标记到此围栏点
mCurrentFence++;

// 向命令队列添加一条用来设置新围栏点的命令
//由于这条命令有交给GPU处理,所以在GPU处理完命令队列中此Signal()的所有命令之前
//它不会设置新的围栏点
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

// 在CPU等待GPU,直到后者执行完这个围栏点之前的所有命令
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

// 若GPU命令中当前的围栏点(即执行到Signal()指令,修改了围栏点),则激发预定事件
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

// 等待GPU命中围栏,激发事件
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
资源转换

资源冒险:当GPU的写操作还没有完成或者还没有开始,却开始读取资源的情况

为了解决资源冒险问题,Direct3D设计了一组相关状态,如资源在创建的时候会初一默认状态,直到应用程序通过方法将其转换为另一种状态。

转换资源屏障(transition resource barrier):通过一个API调用来转换多个资源要用到的数组

初始化Direct3D

  • 初始化流程
  1. D3D12CreateDevice函数创建ID3D12Device接口实例
  2. 创建一个ID3D12Fence对象,并且查询描述符的大小
  3. 检测用户设备对4X MSAA质量级别的支持情况
  4. 一次创建命令队列、命令列表分配器和主命令列表
  5. 描述并创建交换链
  6. 创建应用程序所需的描述符堆
  7. 调整后台缓冲区的大小,并为它创建渲染图标视图
  8. 创建深度/模板缓冲区及与之关联的深度/模板视图
  9. 设置视口(viewport)和裁剪矩形(scissor rectangle)
创建设备
1
2
3
4
5
HRESULT WINAPI D3D12CreateDevice(
_In_opt_ IUnknown* pAdapter,
D3D_FEATURE_LEVEL MinimumFeatureLevel,
_In_ REFIID riid, // Expected: ID3D12Device
_COM_Outptr_opt_ void** ppDevice );

案例

1
2
3
4
5
// Try to create hardware device.
HRESULT hardwareResult = D3D12CreateDevice(
nullptr, // default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice));
  1. pAdapter:使用的显示适配器,如果空,则使用主显示适配器
  2. MinimumFeatureLevele:应用程序需要硬件所支持的最低功能级别,如果适配器不支持此功能级别,则设备创建失败
  3. riid:ID3D12Device接口的COM ID
  4. ppDevice:返回创建的Direct3D 12设备

创建失败的话会尝试创建WARP设备

创建围栏
1
2
3
4
5
6
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));
//查询并保存描述符信息,方便后面使用
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
检测4X MSAA支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // Check 4X MSAA quality support for our back buffer format.
// All Direct3D 11 capable devices support 4X MSAA for all render
// target formats, so we only need to check quality support.

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)));

m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
创建命令队列和命令列表
  • 命令队列:ID3D12CommandQueue

  • 命令分配器:ID3D12CommandAllocator

  • 命令列表:ID3D12GraphicsCommandList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//.h申明
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
//.cpp
void D3DApp::CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));

ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));

ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // 关联的命令分配器
nullptr, //初始的PipelineStateObject
IID_PPV_ARGS(mCommandList.GetAddressOf())));

//从关闭状态开始。 这是因为我们第一次提到
//在命令列表中,我们将对其进行重置,并且需要先关闭它
//调用Reset。
mCommandList->Close();
}
创建交换链
  • DXGI_SWAP_CHAIN_DESC结构体定义
1
2
3
4
5
6
7
8
9
10
11
typedef struct DXGI_SWAP_CHAIN_DESC
{
DXGI_MODE_DESC BufferDesc;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
HWND OutputWindow;
BOOL Windowed;
DXGI_SWAP_EFFECT SwapEffect;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC;
  1. BufferDesc:后台缓冲区的属性,主要是高度、宽度和像素格式
  2. SampleDesc:多重采样的质量级别以及对每个像素的采样次数
  3. BufferUsage:如果要将数据渲染到后台缓冲区,则设置为DXGI_USAGE_RENDER_TARGET_OUTPUT
  4. BufferCount:缓冲区数量,指定为2即双缓冲
  5. OutputWindow:渲染窗口的句柄
  6. Windowed:true则窗口模式运行,否则全屏
  7. SwapEffect:指定为DXGI_SWAP_EFFECT_FLIP_DISCARD
  8. Flags:可选;如果指定为DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH则程序切换为全屏时,将选择适用于当前窗口尺寸的显示模式;否则就采用当前桌面的显示模式
  • DXGI_MODE_DESC
1
2
3
4
5
6
7
8
9
typedef struct DXGI_MODE_DESC
{
UINT Width;//缓冲区分辨率的宽度
UINT Height;//高度
DXGI_RATIONAL RefreshRate;
DXGI_FORMAT Format;//显示格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;//逐行扫面vs.隔行扫描
DXGI_MODE_SCALING Scaling;//如何进行拉升
} DXGI_MODE_DESC;
执行创建交换链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void D3DApp::CreateSwapChain()
{
// 释放我们将重新创建的先前的交换链。
mSwapChain.Reset();

DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth;
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

// Note: 交换链使用队列执行刷新。
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
}
创建描述符堆

创建描述符堆来存储程序中要用到的描述符/视图,本例中需要创建两个描述符堆来存储SwapChainBufferCountRTV ,另外一个存储1个DSV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//.h
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
//.cpp
void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;//static value=2
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));


D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}

创建以后需要通过方法来获得描述符的句柄

1
2
3
4
5
6
7
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const
{//构造函数根据给定的偏移量找到当前后台缓冲区的RTV
return CD3DX12_CPU_DESCRIPTOR_HANDLE(
mRtvHeap->GetCPUDescriptorHandleForHeapStart(),//堆中的首个句柄
mCurrBackBuffer,//偏移至后台缓冲区描述符句柄的索引
mRtvDescriptorSize);//描述符所占字节大小
}
1
2
3
4
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const
{
return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}
创建渲染目标视图

资源不能与渲染流水线中的阶段直接绑定,所以必须先为资源创建视图(描述符),并将其绑定到流水线阶段

1
2
3
4
5
6
virtual HRESULT STDMETHODCALLTYPE GetBuffer( 
/* [in] */ UINT Buffer,
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][out][in] */
_COM_Outptr_ void **ppSurface) = 0;
  1. Buffer:后台缓冲区索引
  2. riid:COM ID
  3. ppSurface:返回ID3D12Resource接口的指针,即后台缓冲区

调用此方法后会增加计数,所以使用后需要释放,需通过ComPtr

然后获得后台缓冲区创建的渲染目标视图

1
2
3
4
virtual void STDMETHODCALLTYPE CreateRenderTargetView( 
_In_opt_ ID3D12Resource *pResource,
_In_opt_ const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
_In_ D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor) = 0;
  1. pResource:指定用作渲染目标的资源
  2. pDesc:指向D3D12_RENDER_TARGET_VIEW_DESC数组结构体的指针
  3. DestDescriptor:引用所创建渲染目标视图的描述符句柄
1
2
3
4
5
6
7
8
9
10
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
//获得交换链中的第 i 个缓冲区
ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
//为此缓冲区创建一个RTV
md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
//偏移到下一个缓冲区
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
创建深度/模板缓冲区及其视图

因为深度缓冲区就是一种2D纹理,所以我们通过填写D3D12_RESOURCE_DESC结构体来描述纹理资源

再用ID3D12Device::CreateCommittedResource方法来创建它

  • D3D12_RESOURCE_DESC
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct D3D12_RESOURCE_DESC
{
D3D12_RESOURCE_DIMENSION Dimension;
UINT64 Alignment;
UINT64 Width;
UINT Height;
UINT16 DepthOrArraySize;
UINT16 MipLevels;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D12_TEXTURE_LAYOUT Layout;
D3D12_RESOURCE_FLAGS Flags;
} D3D12_RESOURCE_DESC;
  1. D3D12_RESOURCE_DIMENSION Dimension:资源的维度

    1. enum D3D12_RESOURCE_DIMENSION
          {
              D3D12_RESOURCE_DIMENSION_UNKNOWN    = 0,
              D3D12_RESOURCE_DIMENSION_BUFFER    = 1,
              D3D12_RESOURCE_DIMENSION_TEXTURE1D    = 2,
              D3D12_RESOURCE_DIMENSION_TEXTURE2D    = 3,
              D3D12_RESOURCE_DIMENSION_TEXTURE3D    = 4
          }     D3D12_RESOURCE_DIMENSION;
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36

      2. Width:像素单位的纹理宽度。对于缓冲区,此项是占用的字节数

      3. Height:同上

      4. DepthOrArraySize:纹素为单位的纹理深度,或者是纹理数组的大小

      5. MipLevels:mipmap层级的数量

      6. Format:DXGI_FORMAT枚举成员之一

      7. SampleDesc:多重采样级别和每个像素的采样次数

      8. Layout:D3D12_TEXTURE_LAYOUT枚举成员之一,用于指定纹理布局

      9. Flags:杂项标记,对于深度/模板缓冲区资源,设置为`D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL`



      > `GPU`资源都存储在`堆`中,本质是具有特定属性的`GPU`显存快
      >
      > `ID3D12Device::CreateCommitedResource`方法根据提供的属性创建一个资源和一个堆,把资源提交到这个堆



      ###### CreateCommittedResource

      ```cpp
      virtual HRESULT STDMETHODCALLTYPE CreateCommittedResource(
      _In_ const D3D12_HEAP_PROPERTIES *pHeapProperties,//资源提交到的堆的属性,见下
      D3D12_HEAP_FLAGS HeapFlags,//额外标记,一般是D3D12_HEAP_FLAG_NONE
      _In_ const D3D12_RESOURCE_DESC *pDesc,//描述待创建的资源
      D3D12_RESOURCE_STATES InitialResourceState,//此参数来设置资源的初始状态
      _In_opt_ const D3D12_CLEAR_VALUE *pOptimizedClearValue,//清楚资源的优化值,不需要就选择nullptr
      REFIID riidResource,//COM ID
      _COM_Outptr_opt_ void **ppvResource) = 0;//新创建的资源,指向ID3D12Resource的指针
D3D12_HEAP_PROPERTIES
1
2
3
4
5
6
7
8
typedef struct D3D12_HEAP_PROPERTIES
{
D3D12_HEAP_TYPE Type;
D3D12_CPU_PAGE_PROPERTY CPUPageProperty;
D3D12_MEMORY_POOL MemoryPoolPreference;
UINT CreationNodeMask;
UINT VisibleNodeMask;
} D3D12_HEAP_PROPERTIES;
D3D12_HEAP_TYPE
1
2
3
4
5
6
7
8
enum D3D12_HEAP_TYPE
{
D3D12_HEAP_TYPE_DEFAULT = 1,//默认堆
D3D12_HEAP_TYPE_UPLOAD = 2,//上传堆
D3D12_HEAP_TYPE_READBACK = 3,//回读堆
D3D12_HEAP_TYPE_CUSTOM = 4//高级场景使用
} D3D12_HEAP_TYPE;

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
1
2
3
4
5
6
7
 // 使用资源格式将描述符创建为整个资源的MIP级别0。
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), nullptr, DepthStencilView());

// 将资源从其初始状态转换为深度缓冲区。
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));

设置视口
1
D3D12_VIEWPORT mScreenViewport; 
1
2
3
4
5
6
7
8
9
typedef struct D3D12_VIEWPORT
{
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;//负责从区间0-1转化为MinDepth-MaxDepth
FLOAT MaxDepth;
} D3D12_VIEWPORT;

填好结构体以后通过函数ID3D12GraphicsCommandList::RSSetViewports方法来设置视口

示例
1
2
3
4
5
6
7
8
9
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width = static_cast<float>(mClientWidth);
mScreenViewport.Height = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;


mCommandList->RSSetViewports(1, &mScreenViewport);

不能为同一个渲染目标指定多个视口

而多个视口则是一种用于对多个渲染目标同时进行渲染的高级技术

命令列表重置,视口也要重置

设置裁剪矩形
1
2
3
4
5
6
7
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;
1
2
3
mScissorRect = { 0, 0, mClientWidth, mClientHeight };

mCommandList->RSSetScissorRects(1, &mScissorRect);

不能为同一个渲染目标指定多个裁剪矩形。

多裁剪矩形是以各种用于同时对多个渲染目标进行渲染的高级技术

裁剪矩形需要随着命令列表重置而重置

计时与动画

性能计时器

QueryPerformanceCounter函数来活得性能计时器测量的当前时刻值

1
2
3
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
mSecondsPerCount = 1.0 / (double)countsPerSec;

通过如下方式转换为秒

1
valueInSecs=valueInCounts * mSecondsPercount;

调用2次QueryPerformanceCounter函数得到两个时间戳的相对插值

1
2
3
4
__int64 A;
QueryPerformanceFrequency((LARGE_INTEGER*)&A);
__int64 B;
QueryPerformanceFrequency((LARGE_INTEGER*)&B);

B-A即可获得执行期间的计数值,或者(B-1)*mSecondsPerCount获得代码运行期间所花费的秒数

游戏计时器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GameTimer
{
public:
GameTimer();

float TotalTime()const; //秒为单位
float DeltaTime()const; //秒为单位

void Reset(); // 开始循环之前调用
void Start(); // 接触计时器暂停时调用
void Stop(); // 暂停计时器调用
void Tick(); //每帧都要调用

private:
double mSecondsPerCount;
double mDeltaTime;

__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
__int64 mPrevTime;
__int64 mCurrTime;

bool mStopped;
};

帧与帧之间的时间间隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void GameTimer::Tick()
{
if( mStopped )
{
mDeltaTime = 0.0;
return;
}
// 获得本帧开始的时刻
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;

// 两帧的时间差
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

// 把当前时间设置为下一次的开始时间
mPrevTime = mCurrTime;

//保证时间差为非负值;
//在处理器处于节能模式或者计算两次时间差的过程中切换到了另一个处理器可能会得到负值
if(mDeltaTime < 0.0)
{
mDeltaTime = 0.0;
}
}

Tick函数被调用在D3DApp::Run函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int D3DApp::Run()
{
MSG msg = {0};

mTimer.Reset();

while(msg.message != WM_QUIT)
{
// If there are Window messages then process them.
if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// Otherwise, do animation/game stuff.
else
{
mTimer.Tick();

if( !mAppPaused )
{
CalculateFrameStats();
Update(mTimer);
Draw(mTimer);
}
else
{
Sleep(100);
}
}
}

return (int)msg.wParam;
}

Reset方法初始化第一帧的数据,因为第一帧没有之前的帧

1
2
3
4
5
6
7
8
9
10
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
总时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
float GameTimer::TotalTime()const
{


//如果我们停止了,请勿计算自停止以来经过的时间。
//此外,如果我们之前已经停顿了一下
// mStopTime-mBaseTime包含暂停时间,我们不想计算。
//要纠正此问题,我们可以从mStopTime中减去暂停时间:
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------------*------> time
// mBaseTime mStopTime startTime mStopTime mCurrTime

if( mStopped )
{
return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
}

//距离mCurrTime-mBaseTime包括暂停时间,
//我们不想计算。 要纠正这一点,我们可以减去
//从mCurrTime暂停的时间:
//
// (mCurrTime - mPausedTime) - mBaseTime
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------> time
// mBaseTime mStopTime startTime mCurrTime

else
{
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}

应用程序框架示例

自己模仿框架的代码

DXApp.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#pragma once
#include "../Common/d3dUtil.h"
#include "../Common/d3dx12.h"
#include "../Common/GameTimer.h"

#if defined(DEBUG) || defined(_DEBUG)
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#endif
#include <dxgiformat.h>

// Link necessary d3d12 libraries.
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib, "D3D12.lib")
#pragma comment(lib, "dxgi.lib")




class DXApp
{

public:
DXApp(HINSTANCE nInstance);
DXApp(const DXApp& rhs) = delete;
DXApp& operator=(const DXApp& rhs) = delete;
virtual ~DXApp();

protected:
virtual void OnResize();
virtual bool Initialize();
virtual void CreateRtvAndDsvDescriptorHeaps();//创建描述符堆
virtual void Update(const GameTimer& gt) = 0;
virtual void Draw(const GameTimer& gt) = 0;

bool InitMainWindow();//初始化窗口
bool InitDirect3D();//初始化DX


void FlushCommandQueue();//齐平命令队列
void CreateSwapChain();//创建交换链
void CreateCommandObjects();//创建命令队列、命令适配器、命令列表


ID3D12Resource* CurrentBackBuffer()const;
D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const;
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const;


void LogAdapters();
void LogAdapterOutputs(IDXGIAdapter* adapter);
void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format);
public:
static DXApp* GetApp();
HINSTANCE AppInst()const;//得到应用程序实例
HWND MainWnd()const;//得到窗口
float AspectRatio()const;//长宽比
bool Get4xMsaaState()const;//4XMSAA开启与否
void Set4xMsaaState(bool value);//设置4XMSAA

int Run();
void CalculateFrameStats();
/*
HWND hwnd; //窗口句柄
UINT message; //消息常量标识符
WPARAM wParam; //32位消息的特定附加信息,具体表示什么处决于message
LPARAM lParam; //32位消息的特定附加信息,具体表示什么处决于message
*/
virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

//鼠标输入
virtual void OnMouseDown(WPARAM btnState, int x, int y) { }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y) { }



protected:

static DXApp* mApp;

HINSTANCE mhAppInst = nullptr; // 应用程序实例句柄
HWND mhMainWnd = nullptr; // 主窗口句柄
bool mAppPaused = false; // 应用程序是否已暂停?
bool mMinimized = false; // 将应用程序最小化?
bool mMaximized = false; // 应用程序是否已最大化?
bool mResizing = false; // 是否拖动了大小调整栏?
bool mFullscreenState = false;// 启用全屏

// 是否启用 4X MSAA (?.1.8). The default is false.
bool m4xMsaaState = false; // 启用 4X MSAA
UINT m4xMsaaQuality = 0; // 4倍MSAA的质量水平

GameTimer mTimer;

//COM
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice; //设备接口
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;//交换链

Microsoft::WRL::ComPtr<ID3D12Fence> mFence;//围栏
UINT64 mCurrentFence = 0;

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;//命令队列
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;//适配器
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;//列表
//描述符
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;

static const int SwapChainBufferCount = 2;//双重缓冲
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];//交换链缓冲区数组
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;//深度模板缓冲区

Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;//RTV描述符堆
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;//DSV描述符堆

std::wstring mMainWndCaption = L"DX App";
D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;

D3D12_VIEWPORT mScreenViewport;//视口
D3D12_RECT mScissorRect;//裁剪

int mClientWidth = 800;
int mClientHeight = 600;
};
DXApp.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
#include "DXApp.h"
#include "WindowsX.h"
#include <winuser.h>
#include <dxgi.h>
#include <iostream>

using Microsoft::WRL::ComPtr;
using namespace std;
using namespace DirectX;



DXApp* DXApp::mApp = nullptr;

LRESULT CALLBACK
MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
//cout << "test";
return DXApp::GetApp()->MsgProc(hwnd, msg, wParam, lParam);
}



DXApp::DXApp(HINSTANCE nInstance):mhAppInst(nInstance)
{
assert(mApp == nullptr);
mApp = this;
}

DXApp::~DXApp()
{
if (mhAppInst != nullptr)
{
FlushCommandQueue();
}
}

void DXApp::OnResize()
{
assert(md3dDevice);
assert(mSwapChain);
assert(mDirectCmdListAlloc);

FlushCommandQueue();

ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(),nullptr));

//释放我们将重新创建的先前资源。
for (int i=0;i<SwapChainBufferCount;++i)
{
mSwapChainBuffer[i].Reset();
}
mDepthStencilBuffer.Reset();


//调整交换链的大小。
ThrowIfFailed(mSwapChain->ResizeBuffers(
SwapChainBufferCount,
mClientWidth,
mClientHeight,
mBackBufferFormat,
DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));

mCurrBackBuffer=0;

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
//构造函数根据给定的偏移量找到当前后台缓冲区的RTV
ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);//堆中的首个句柄
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

//创建深度/模板缓冲区和视口
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension= D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment=0;
depthStencilDesc.Width=mClientWidth;
depthStencilDesc.Height=mClientHeight;
depthStencilDesc.DepthOrArraySize=1;
depthStencilDesc.MipLevels=1;
depthStencilDesc.Format=mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count=m4xMsaaState?4:1;
depthStencilDesc.SampleDesc.Quality=m4xMsaaState?(m4xMsaaQuality-1):0;
depthStencilDesc.Layout=D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags=D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

D3D12_CLEAR_VALUE optClear;
optClear.Format=mDepthStencilFormat;
optClear.DepthStencil.Depth=1.0f;
optClear.DepthStencil.Stencil=0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));

//使用资源的格式将描述符创建为整个资源的MIP级别0。
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(),nullptr, DepthStencilView());
//将资源从其初始状态转换为深度缓冲区。
mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),D3D12_RESOURCE_STATE_COMMON,D3D12_RESOURCE_STATE_DEPTH_WRITE));
//执行resize命令
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[]={mCommandList.Get()};
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists),cmdsLists);

//等待到resize完成
FlushCommandQueue();

//更新视口变换以覆盖客户区域。
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width = static_cast<float>(mClientWidth);
mScreenViewport.Height = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;

mScissorRect={0,0,mClientWidth,mClientHeight};
}

bool DXApp::Initialize()
{
if (!InitMainWindow())
{
return false;
}
if (!InitDirect3D())
{
return false;
}
OnResize();
return true;
}

void DXApp::CreateRtvAndDsvDescriptorHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors=SwapChainBufferCount;
rtvHeapDesc.Type=D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags=D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask=0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&rtvHeapDesc,IID_PPV_ARGS(mRtvHeap.GetAddressOf())));

D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = SwapChainBufferCount;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

}

bool DXApp::InitMainWindow()
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = mhAppInst;
wc.hIcon = LoadIcon(0, IDI_APPLICATION);
wc.hCursor = LoadCursor(0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"MainWnd";

if (!RegisterClass(&wc))
{
MessageBox(0, L"RegisterClass Failed.", 0, 0);
return false;
}

// Compute window rectangle dimensions based on requested client area dimensions.
RECT R = { 0, 0, mClientWidth, mClientHeight };
AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
int width = R.right - R.left;
int height = R.bottom - R.top;

mhMainWnd = CreateWindow(L"MainWnd", mMainWndCaption.c_str(),
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
if (!mhMainWnd)
{
MessageBox(0, L"CreateWindow Failed.", 0, 0);
return false;
}

ShowWindow(mhMainWnd, SW_SHOW);
UpdateWindow(mhMainWnd);

return true;
}



bool DXApp::InitDirect3D()
{
// {
//#if defined(DEBUG) || defined(_DEBUG)
// // Enable the D3D12 debug layer.
// {
// ComPtr<ID3D12Debug> debugController;
// ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
// debugController->EnableDebugLayer();
// }
//#endif
ThrowIfFailed(CreateDXGIFactory(IID_PPV_ARGS(&mdxgiFactory)));

//创建硬件设备
HRESULT hardwareResult = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&md3dDevice));//第一个参数是空则使用默认显示器
//回退至WARP设备
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(pWarpAdapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&md3dDevice)));
}
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));

//描述符信息,方便以后使用
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

//检查4X MSAA质量对我们的后缓冲区格式的支持。
//所有支持Direct3D 11的设备都对所有渲染支持4倍MSAA
//目标格式,因此我们只需要检查质量支持。
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &msQualityLevels, sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");


CreateCommandObjects();
CreateSwapChain();
CreateRtvAndDsvDescriptorHeaps();

return true;
}

void DXApp::FlushCommandQueue()
{
//提升围栏值以将命令标记到该围栏点。
mCurrentFence++;
//将指令添加到命令队列以设置新的防护点。 因为我们
//在GPU时间轴上,直到GPU完成后才会设置新的围栏点
//处理此Signal()之前的所有命令。
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));

//等待直到GPU完成命令为止。
if (mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle=CreateEventEx(nullptr,false,false,EVENT_ALL_ACCESS);
//GPU击中当前围墙时触发事件。
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence,eventHandle));

//等到GPU击中当前的fence事件后再触发。
WaitForSingleObject(eventHandle,INFINITE);
CloseHandle(eventHandle);
}
}

void DXApp::CreateSwapChain()
{
mSwapChain.Reset();
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth;
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

// Note: Swap chain uses queue to perform flush.
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
}

void DXApp::CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
ThrowIfFailed(md3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
ThrowIfFailed(md3dDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, mDirectCmdListAlloc.Get(), nullptr, IID_PPV_ARGS(mCommandList.ReleaseAndGetAddressOf())));

//从关闭状态开始。 这是因为我们第一次提到
//在命令列表中,我们将对其进行重置,并且需要先关闭它
//调用Reset。
mCommandList->Close();
}

ID3D12Resource* DXApp::CurrentBackBuffer() const
{
return mSwapChainBuffer[mCurrBackBuffer].Get();
}

D3D12_CPU_DESCRIPTOR_HANDLE DXApp::CurrentBackBufferView() const
{
return CD3DX12_CPU_DESCRIPTOR_HANDLE(
mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
mCurrBackBuffer,
mRtvDescriptorSize);
}

D3D12_CPU_DESCRIPTOR_HANDLE DXApp::DepthStencilView() const
{
return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}


void DXApp::LogAdapters()
{
UINT i = 0;
IDXGIAdapter* adapter = nullptr;
std::vector<IDXGIAdapter*> adapterList;
while (mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC desc;
adapter->GetDesc(&desc);

std::wstring text = L"***Adapter: ";
text += desc.Description;
text += L"\n";

OutputDebugString(text.c_str());

adapterList.push_back(adapter);

++i;
}

for (size_t i = 0; i < adapterList.size(); ++i)
{
LogAdapterOutputs(adapterList[i]);
ReleaseCom(adapterList[i]);
}
}

void DXApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
UINT i = 0;
IDXGIOutput* output = nullptr;
while (adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
{
DXGI_OUTPUT_DESC desc;
output->GetDesc(&desc);

std::wstring text = L"***Output: ";
text += desc.DeviceName;
text += L"\n";
OutputDebugString(text.c_str());

LogOutputDisplayModes(output, mBackBufferFormat);

ReleaseCom(output);

++i;
}
}

void DXApp::LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
{
UINT count = 0;
UINT flags = 0;

// Call with nullptr to get list count.
output->GetDisplayModeList(format, flags, &count, nullptr);

std::vector<DXGI_MODE_DESC> modeList(count);
output->GetDisplayModeList(format, flags, &count, &modeList[0]);

for (auto& x : modeList)
{
UINT n = x.RefreshRate.Numerator;
UINT d = x.RefreshRate.Denominator;
std::wstring text =
L"Width = " + std::to_wstring(x.Width) + L" " +
L"Height = " + std::to_wstring(x.Height) + L" " +
L"Refresh = " + std::to_wstring(n) + L"/" + std::to_wstring(d) +
L"\n";

::OutputDebugString(text.c_str());
}
}

DXApp* DXApp::GetApp()
{
return mApp;
}

HINSTANCE DXApp::AppInst() const
{
return mhAppInst;
}

HWND DXApp::MainWnd() const
{
return mhMainWnd;
}

float DXApp::AspectRatio() const
{
return static_cast<float>(mClientWidth) / mClientHeight;
}

bool DXApp::Get4xMsaaState() const
{
return m4xMsaaState;
}

void DXApp::Set4xMsaaState(bool value)
{
if (m4xMsaaState!=value)
{
m4xMsaaState = value;
//重置4xmasaa需要重新创建交换链和刷新尺寸
CreateSwapChain();
OnResize();

}
}

int DXApp::Run()
{
MSG msg = { 0 };

mTimer.Reset();

while (msg.message != WM_QUIT)
{
// If there are Window messages then process them.
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Otherwise, do animation/game stuff.
else
{
mTimer.Tick();

if (!mAppPaused)
{
CalculateFrameStats();
Update(mTimer);
Draw(mTimer);
}
else
{
Sleep(100);
}
}
}

return (int)msg.wParam;

}

void DXApp::CalculateFrameStats()
{
// Code computes the average frames per second, and also the
// average time it takes to render one frame. These stats
// are appended to the window caption bar.

static int frameCnt = 0;
static float timeElapsed = 0.0f;

frameCnt++;

// Compute averages over one second period.
if ((mTimer.TotalTime() - timeElapsed) >= 1.0f)
{
float fps = (float)frameCnt; // fps = frameCnt / 1
float mspf = 1000.0f / fps;

wstring fpsStr = to_wstring(fps);
wstring mspfStr = to_wstring(mspf);

wstring windowText = mMainWndCaption +
L" fps: " + fpsStr +
L" mspf: " + mspfStr;

SetWindowText(mhMainWnd, L"window text");

// Reset for next average.
frameCnt = 0;
timeElapsed += 1.0f;
}
}

LRESULT DXApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
//激活或停用窗口时发送WM_ACTIVATE。
//当停用窗口时我们暂停游戏
//当它变为活动状态时取消暂停
case WM_ACTIVATE:
if (LOWORD(wParam)==WA_INACTIVE)
{
mAppPaused = true;
mTimer.Stop();
}
else
{
mAppPaused = false;
mTimer.Start();
}
return 0;
// 当用户调整窗口大小时,发送WM_SIZE。
case WM_SIZE:
// 保存新的客户区域尺寸。
mClientWidth = LOWORD(lParam);
mClientHeight = HIWORD(lParam);
if (md3dDevice)
{
if (wParam == SIZE_MINIMIZED)
{
mAppPaused = true;
mMinimized = true;
mMaximized = false;
}
else if (wParam == SIZE_MAXIMIZED)
{
mAppPaused = false;
mMinimized = false;
mMaximized = true;
OnResize();
}
else if (wParam == SIZE_RESTORED)
{

// Restoring from minimized state?
if (mMinimized)
{
mAppPaused = false;
mMinimized = false;
OnResize();
}

// Restoring from maximized state?
else if (mMaximized)
{
mAppPaused = false;
mMaximized = false;
OnResize();
}
else if (mResizing)
{
//如果用户拖动调整大小栏,我们不会调整大小
//这里的缓冲区,因为随着用户的不断前进
//拖动调整大小条,WM_SIZE消息流是
//发送到窗口,它将毫无意义(而且很慢)
//为从拖动中收到的每个WM_SIZE消息调整大小
//调整尺寸栏。 因此,我们在用户
//完成调整窗口大小并释放大小调整条
//发送WM_EXITSIZEMOVE消息。
}
else // API call such as SetWindowPos or mSwapChain->SetFullscreenState.
{
OnResize();
}
}
}
return 0;

// 当用户抓住调整大小条时,将发送WM_EXITSIZEMOVE。
case WM_ENTERSIZEMOVE:
mAppPaused = true;
mResizing = true;
mTimer.Stop();
return 0;

//当用户释放大小调整条时,发送WM_EXITSIZEMOVE。
//在这里,我们根据新窗口的尺寸重置所有内容。
case WM_EXITSIZEMOVE:
mAppPaused = false;
mResizing = false;
mTimer.Start();
OnResize();
return 0;
//销毁窗口时发送WM_DESTROY。
case WM_DESTROY:
PostQuitMessage(0);
return 0;
// 当菜单处于活动状态并且用户按下时,将发送WM_MENUCHAR消息与任何助记键或加速键都不对应的键。
case WM_MENUCHAR:
// 进入时不要发出哔声。
return MAKELRESULT(0, MNC_CLOSE);


//捕获此消息,以防止窗口变得太小。
case WM_GETMINMAXINFO:
((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
return 0;

case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_MOUSEMOVE:
OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_KEYUP:
if (wParam == VK_ESCAPE)
{
PostQuitMessage(0);
}
else if ((int)wParam == VK_F2)
Set4xMsaaState(!m4xMsaaState);

return 0;
}


return DefWindowProc(hwnd, msg, wParam, lParam);
}


Instructions

  • It is very important that you should click the option button to set your keys ,and save it
  • Click the Multiplayer button to enter the character selection interface
    • V1.0 has no AI so we can not click SinglePlayer button
  • Use the LEFT and RIGHT buttons and A button in your button settings to select and confirm the role
    • ps. A Key is not the keyboard A, but the A in the game, generally the default is U key
  • Then player 1 can use the same operation to select the map
  • After that,let’s battle!

Charcater Moves

WuKong

NormalMoves
Keys Moves(temp name)
+A/C 1
+A/C 2
+B/D 3
+A/C 4
SpecialMoves
Keys Moves(temp name)
+A/C 1
+A/C 2
MaxSpecialMoves
Keys Moves(temp name)
(In Max Stat)+A/C 1

Aurora

NormalMoves
Keys Moves(temp name)
+A/C 1
+A/C 2
+B/D 3
+B/D 4
SpecialMoves
Keys Moves(temp name)
+A/C 1
+B/D 2
MaxSpecialMoves
Keys Moves(temp name)
(In Max Stat)+A/C 1

本文介绍路径点的优化方式,或者叫做多线段优化、轨迹点优化

根据设定阈值,去掉路径点中的部分多余点,以达到方便传输的目的

本文用UE4蓝图和C++蓝图函数库的2个方式解释

注:多种方法可以叠加使用

本算法提供了基于UE4的Demo,PC安卓

GitHub工程下载

Demo演示动图

阅读全文 »

角色位移本质上就是渲染问题,从这条思路我们去看看角色怎么实现这一部分渲染的

起点:SetActorLocation

蓝图调用的API实际调用的是K2_SetActorLocation,然后调用到此函数

1
return RootComponent->MoveComponent(Delta, GetActorQuat(), bSweep, OutSweepHitResult, MOVECOMP_NoFlags, Teleport);

只看这个关键代码,调用RootComponentMoveComponent

USceneComponent
MoveComponent

外部调用此函数后里面直接调用到带Impl后缀的虚函数

这个函数在UPrimitiveComponent内有重写

USceneComponent内主要调用了函数ConditionalUpdateComponentToWorld,而UPrimitiveComponent因为有碰撞等属性所以又多了很多逻辑,这里不多描述

UpdateComponentToWorldWithParent

此函数通过ConditionalUpdateComponentToWorld调用,后又调用到虚函数UpdateComponentToWorld,此虚函数在UActorComponent声明,在USceneComponent重写

然后再直接调用到此函数

1
2
3
4
5
6
7
8
9
if (Parent && !Parent->bComponentToWorldUpdated)
{
Parent->UpdateComponentToWorld();

if (bComponentToWorldUpdated)
{
return;
}
}

如果有父级组件,先去处理他

然后调用到函数PropagateTransformUpdate

PropagateTransformUpdate

该函数执行了

  • 更新体积(Bounds)
  • 更新变换(Transform)
    • 更新Attach的子组件的变换信息
  • 更新寻路信息

其中有个关键的函数UActorComponent::MarkRenderTransformDirty

然后经过简单判断以后调用UActorComponent::MarkForNeededEndOfFrameUpdate

然后调用了UWorldMarkActorComponentForNeededEndOfFrameUpdate

看主要代码

1
2
3
4
5
6
7
8
void UActorComponent::MarkRenderTransformDirty()
{
if (IsRegistered() && bRenderStateCreated)
{
bRenderTransformDirty = true;
MarkForNeededEndOfFrameUpdate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UActorComponent::MarkForNeededEndOfFrameUpdate()
{
if (bNeverNeedsRenderUpdate)
{
return;
}

UWorld* ComponentWorld = GetWorld();
if (ComponentWorld)
{
ComponentWorld->MarkActorComponentForNeededEndOfFrameUpdate(this, RequiresGameThreadEndOfFrameUpdates());
}
else if (!IsUnreachable())
{
// 如果没有世界,执行如下代码
DoDeferredRenderUpdates_Concurrent();
}
}
UWorld
MarkActorComponentForNeededEndOfFrameUpdate

代码在LevelTick.cpp L883

主要执行了把组件添加到了等待渲染的列表ComponentsThatNeedEndOfFrameUpdate_OnGameThread里,当然还有一个非游戏线程的ComponentsThatNeedEndOfFrameUpdate

1
2
3
4
5
6
7
8
9
10
if (bForceGameThread)
{
FMarkComponentEndOfFrameUpdateState::Set(Component, ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Num(), EComponentMarkedForEndOfFrameUpdateState::MarkedForGameThread);
ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Add(Component);
}
else
{
FMarkComponentEndOfFrameUpdateState::Set(Component, ComponentsThatNeedEndOfFrameUpdate.Num(), EComponentMarkedForEndOfFrameUpdateState::Marked);
ComponentsThatNeedEndOfFrameUpdate.Add(Component);
}

到此我们大概知道了这一条线的终点了,然后去看看哪里使用到了这个数组

SendAllEndOfFrameUpdates

确认使用上面数组是在此函数内,查找引用发现此函数在很多地方被调用,如SceneRendering.cpp,UnrealEngine.cpp大概就是要更新场景最新的渲染的时候就会调用,先不理会,看这个函数内实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (UActorComponent* Component : ComponentsThatNeedEndOfFrameUpdate_OnGameThread)
{
if (Component)
{
if (Component->IsRegistered() && !Component->IsTemplate() && !Component->IsPendingKill())
{
FScopeCycleCounterUObject ComponentScope(Component);
FScopeCycleCounterUObject AdditionalScope(STATS ? Component->AdditionalStatObject() : nullptr);
Component->DoDeferredRenderUpdates_Concurrent();
}

check(Component->IsPendingKill() || Component->GetMarkedForEndOfFrameUpdateState() == EComponentMarkedForEndOfFrameUpdateState::MarkedForGameThread);
FMarkComponentEndOfFrameUpdateState::Set(Component, INDEX_NONE, EComponentMarkedForEndOfFrameUpdateState::Unmarked);
}
}
ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Reset();
ComponentsThatNeedEndOfFrameUpdate.Reset();

遍历了所有数组成员,主要执行了DoDeferredRenderUpdates_Concurrent,回头看USceneComponent内的如果没有UWorld就直接调用了这个函数

UActorComponent
DoDeferredRenderUpdates_Concurrent

RecreateRenderState_Concurrent,SendRenderTransform_Concurrent,SendRenderDynamicData_Concurrent告诉引擎需要处理相应的渲染任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void UActorComponent::DoDeferredRenderUpdates_Concurrent()
{
checkf(!IsUnreachable(), TEXT("%s"), *GetFullName());
checkf(!IsTemplate(), TEXT("%s"), *GetFullName());
checkf(!IsPendingKill(), TEXT("%s"), *GetFullName());

FScopeCycleCounterUObject ContextScope(this);

if(!IsRegistered())
{
UE_LOG(LogActorComponent, Log, TEXT("UpdateComponent: (%s) Not registered, Aborting."), *GetPathName());
return;
}

if(bRenderStateDirty)
{
SCOPE_CYCLE_COUNTER(STAT_PostTickComponentRecreate);
RecreateRenderState_Concurrent();
checkf(!bRenderStateDirty, TEXT("Failed to route CreateRenderState_Concurrent (%s)"), *GetFullName());
}
else
{
SCOPE_CYCLE_COUNTER(STAT_PostTickComponentLW);
if(bRenderTransformDirty)
{
// Update the component's transform if the actor has been moved since it was last updated.
SendRenderTransform_Concurrent();
}

if(bRenderDynamicDataDirty)
{
SendRenderDynamicData_Concurrent();
}
}
}
SendRenderTransform_Concurrent

这个函数在基类只做了基础实现,主要是派生类重写实现主要逻辑,类似的也包括SendRenderDynamicData_Concurrent,CreateRenderState_Concurrent

1
2
3
4
5
6
7
8
{
check(bRenderStateCreated);
bRenderTransformDirty = false;

#if LOG_RENDER_STATE
UE_LOG(LogActorComponent, Log, TEXT("SendRenderTransform_Concurrent: %s"), *GetPathName());
#endif
}
UPrimitiveComponent

紧接上面,我们看看有形状体积等信息的UPrimitiveComponent

SendRenderTransform_Concurrent
1
2
3
4
5
6
7
8
9
10
11
void UPrimitiveComponent::SendRenderTransform_Concurrent()
{
UpdateBounds();
const bool bDetailModeAllowsRendering = DetailMode <= GetCachedScalabilityCVars().DetailMode;
if( bDetailModeAllowsRendering && (ShouldRender() || bCastHiddenShadow))
{
GetWorld()->Scene->UpdatePrimitiveTransform(this);
}

Super::SendRenderTransform_Concurrent();
}

看到在实现基类逻辑之前执行了FScene内的函数UpdatePrimitiveTransform

FScene

代码在 RendererScene.cpp中

UpdatePrimitiveTransform

UpdatePrimitiveTransform函数中会判断该Primitive有没有SceneProxy,如果有SceneProxy则判断该Primitive是否需要重新创建,如果需要重新创建则需要先删除,然后再添加该Primitive;如果没有SceneProxy则直接加入到场景中。接着我们具体看一下AddPrimitive函数的具体实现:

AddPrimitive

此函数主要做了如下几件事情

  • 创建SceneProxyPrimitiveSceneInfo
  • 利用FPrimitiveSceneInfo封装UPrimitiveComponent的信息,继续在render线程中进行操作
  • 给渲染线程发送2个命令,分别是设置PrimitiveTransform信息和将PrimitiveSceneInfo加入到渲染线程的数据集合中

代码如下

1
2
3
4
5
FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
Primitive->SceneProxy = PrimitiveSceneProxy;
// Create the primitive scene info.
FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
1
2
3
4
5
6
7
8
9
10
11
// Create any RenderThreadResources required.
ENQUEUE_RENDER_COMMAND(CreateRenderThreadResourcesCommand)(
[Params](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
FScopeCycleCounter Context(SceneProxy->GetStatId());
SceneProxy->SetTransform(Params.RenderMatrix, Params.WorldBounds, Params.LocalBounds, Params.AttachmentRootPosition);

// Create any RenderThreadResources required.
SceneProxy->CreateRenderThreadResources();
});
1
2
3
4
5
6
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Scene, PrimitiveSceneInfo](FRHICommandListImmediate& RHICmdList)
{
FScopeCycleCounter Context(PrimitiveSceneInfo->Proxy->GetStatId());
Scene->AddPrimitiveSceneInfo_RenderThread(RHICmdList, PrimitiveSceneInfo);
});
AddPrimitiveSceneInfo_RenderThread

AddPrimitiveSceneInfo_RenderThread函数是在渲染线程执行的,完成PrimitiveBoundsPrimitiveFlagsCompact等数据的初始化后,并调用LinkAttachmentGroup处理primitive的父子关系,调用LinkLODParentComponent处理LOD父子关系,然后调用FPrimitiveSceneInfo::AddToScene

AddToScene最终会把物体添加到列表DrawList 中去完成绘制,这里不继续深挖了,埋个扣子,以后来解开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Add the primitive to its shadow parent's linked list of children.
// Note: must happen before AddToScene because AddToScene depends on LightingAttachmentRoot
PrimitiveSceneInfo->LinkAttachmentGroup();

// Set lod Parent information if valid
PrimitiveSceneInfo->LinkLODParentComponent();

// Add the primitive to the scene.
const bool bAddToDrawLists = !(CVarDoLazyStaticMeshUpdate.GetValueOnRenderThread() && !WITH_EDITOR);
if (bAddToDrawLists)
{
PrimitiveSceneInfo->AddToScene(RHICmdList, true);
}
else
{
PrimitiveSceneInfo->AddToScene(RHICmdList, true, false);
PrimitiveSceneInfo->BeginDeferredUpdateStaticMeshes();
}
小结

由此我们就大致整理一下思路

设置Actor的位置,其实就是把新旧位置做一个插值,把这个插值传递给继承自USceneComponentMoveComponent方法,然后在下一帧渲染出新的位置

这里也接上了并解释了之前AI_MoveTo的内容了

上一张流程简图

补充
场景中静态物体的渲染顺序堆栈表参考
1
2
3
4
5
6
7
8
9
10
11
ActorComponet::ExecuteRegisterEvents
UPrimitiveComponent::CreateRenderState_Concurrent
FScene:: AddPrimitive
FScene:: AddPrimitiveSceneInfo_RenderThread
FScene:: AddStaticMeshes
FStaticMesh::AddToDrawLists
FMobileBasePassOpaqueDrawingPolicyFactory::AddStaticMesh
ProcessMobileBasePassMesh
FDrawMobileBasePassStaticMeshAction:: Process
AddMeshToStaticDrawList
//加入到scene GetMobileBasePassDrawList中

最终加入队列 以DrawingPolicykey map队列

1
TStaticMeshDrawList<TMobileBasePassDrawingPolicy<FUniformLightMapPolicy, 0> > MobileBasePassUniformLightMapPolicyDrawList[EBasePass_MAX];

前言

如图所示,我们经常在UE4内看到如此的异步节点,简单说此类节点的输出并非像函数一样瞬间完成,而是拥有自己的生命周期,此类节点一般在右上角有一个时钟标志

本文讲解如何制作类似AI_MoveTo的异步节点

另一篇AI_MoveTo简单分析和扩展介绍AI_MoveTo的简单运行机制和其他扩展方式


  • 2020.11.24更新:

更新各类移动扩展函数库以及K2Node

github

image-20201124134801666

  • 2021.2.25更新:

基于UBlueprintAsyncActionBase的异步节点 跳转

阅读全文 »

UK2Node_AIMoveTo

这是我们最熟悉的编辑器模式下的AI_MoveTo节点,也就是那个自带OnSuccess和OnFailed的异步节点,在蓝图中大多数时候使用起来都得心应手非常方便,但是它有个最大的缺点,异步节点意味着他有自己的生命周期,不能放到函数中使用,同样的,在代码中也没有此类节点,那么接下来我们尝试解决这一问题

翻看此节点的代码,没几行代码,主要看如下

阅读全文 »

UE4自带的DestructibleMesh系统的破碎方式比较单一,可以用英伟达的ApexPhysXLab工具制作自定义破碎文件,然后导入UE4

本文介绍通过使用这一工具制作UE4破碎物体的流程

工具下载地址点击下载

PhysXLab流程

如下图所示,导入3D模型

三种破碎方式

Slice模式

如下图所示,红色框内为主要设置的参数

效果如下

Cutout模式

主要用黑白通道图来切割,注意黑白通道只是平面投射,跟UV无关,所以纹理如果要配合切割需要单独制作配合好的纹理及黑白图

效果如下

Voronoi模式

类似UE4内的破碎

效果如下

导出

UE4流程

用一个模拟子弹来制造破碎效果

最近由于项目需要用到鼠标在场景利绘制曲线,方案有很多,比如SplineMesh,ProceduralMesh 等等,回头一想用特效也可以试试

本案例使用虚幻4.22版本演示

用Niagara的Ribbon绘制样条线的功能

模块函数 NiagaraModuleScript
  • 模块函数是Niagara系统的函数库,是niagara系统最主要区别于老例子系统的其中一点

创建函数库 NM_Test1

完成如上节点连接

注意:变量的前缀是对应模块,这个不能随意更改,创建的时候就选好模块会自动添加前缀;如果是系统已有变量,就不需要自己更改名称,如Emitter.age,表示发射器的生命周期

  • User.SP:User分类下的Spline变量,本案例用下面参数代替
  • Module.Count:函数输入参数,本函数中用于控制发射数量
  • SampleSplinePositionByUnitDistanceWS:输入0-1输出曲线从开始到结束的世界空间位置
  • ExecutionIndex:当前粒子的序号

简单描述这个函数的功能就是根据发射粒子在所有发射粒子中的百分比对应到曲线spline的长度百分比

粒子发射器 Emitter

随意创建一个发射器,删除多余节点,配置图所示

刚开始比较容易混淆的概念是 Emitter的spawn,update与Particle的Spawn,Update的概念

  • Emitter针对的就是整个发射器的操作,约等于老系统的Required和部分Spawn的内容,如整个发射器的生命周期,是否循环
  • Particle控制每个粒子的属性和功能,如每个粒子的大小颜色等
  • 不恰当的比喻就是Emitter就是Actor,Particle就是Component

图中关键节点解释

  • 创建3个User变量,User变量可以到外部设置
  • 创建Emitter模块的Emitter.SpawnCount变量,暴力一点可以直接跳过直接用User的
  • 发射器创建的时候用User.SpawnCount设置Emitter.SpawnCount
  • Emitter.SpawnCount去设置SpawnBurstInstaneous的发射数量
  • 修改Render模块中的粒子类型为Ribbon
  • 设置Ribbon的宽度
  • 在Spawn(在Update也可以) 中加上自定义的函数NM_Test1,并把EmitterSpawnCount设置为输入参数
粒子系统 NiagaraSystem

汇总所有Emitter的集合,同时可以实例化到场景或者类里的类;同时也可以再次修改Emitter中所有参数

如图所示,可以使用多个发射器,可以设置User模块的参数,可以拖拽不同发射器的作用时间(类似动画)

本案例中只需要添加一个发射器

画线类 DrawItem

  • 目前版本貌似没有更直观的方法设置Spline变量,强行用设置Actor变量尽然是可以的(lll¬ω¬)

  • 提供对外函数AddPoint(FVector)用于添加曲线点

PlayerController

不要忘记开启项目设置里Input模块下的UseMouseForTouch

蓝图比较简单,简单描述就是在鼠标按下移动后用射线点通过DrawItem添加曲线点

总结
  1. 用Niagara类绘制曲线模型算是比较简单方便的,Niagara在性能优化上也不错
  2. 曲线在长度到一定程度后会有明显分段情况出现,不够圆滑,目前版本没有公开Ribbon的设置段数的变量
  3. 据说Niagara在4.25才是正式版,目前无法保证bug问题

这是一份Multiplayer RPG Template的说明文档,内容包含其中主要系统的配置和制作方法

此案例是基于UE4的ListerServer建立的局域网联机游戏demo,参考作品WOW

基于UE4版本4.25

demo尚且有未完善的地方,后续会跟进优化和修复

技能系统

如上图所示,此图为技能表的配置信息,下面简述其中重要的参数

  • Action: 这个变量代表这个行为的唯一ID,原则上跟表格的RowName相同,很多逻辑中需要通过这个ID查找关于这个技能的所有信息,或者通过这个ID执行这个技能等
  • Display(struct):3个参数用来显示这个技能一般信息,比如名字或者说明还有图标的显示,在UMG上有相关的体现,当然你也可以用来自定义
  • CastData(struct):这个是一个施法的结构体,主要设置的是施法的方式和条件
    • Type:比如默认的读条施法,持续施法,攻击叠加类的施法等方式
    • Time:施法所需要的时长,0意味着瞬发
    • Duration:如果是持续施法,那么这个参数意味着引导时间
    • KeepAttackStat:施法完以后会继续进行普通攻击
    • MotionCast:移动施法,设置为否的话必须站立施法,一旦移动就打断施法
    • NotTargetCast:不需要目标的施法,一般用于范围技能比如火雨之类
  • Mp:消耗魔法值,战士为怒气值
  • CD:冷却时间
  • MaxRange:最大释放范围,按照虚幻单位计算,0意味着无限制
  • LearnGold/Level:学习该技能的前置条件,0意味着无要求
  • RangeSelectRange:如果是范围选择的技能,此参数表示作用的半径
  • TargetType:目标类型,作用地方还是友方等
  • SkillClass:技能实例的类,无需手动创建
SkillRef

这个是通过表格配置的SkillClass创建的,无需手动创建,你只需要制作这个类的逻辑和效果

这个类是继承自BP_BaseSkill,里面有一些乱七八糟的蓝图逻辑,不过并不是全部都是必须使用的,这个类很多情况下你可以自由发挥,反正技能释放了以后就通过这个类来控制这个技能实例的运行

下面就通过IceBullet这个技能类来说明一下

IceBullet继承于ProjectileBase,如上图所示,这是这个类的基本参数类型

  • speed:飞行速度,依赖ProjectileComponent
  • StopDistance:停止的距离,因为这个类是追踪目标飞行的,所以会总有接近并且到达目标的时候,到达目标并停止以后会产生一段逻辑
  • Duration:持续时间,如果没有命中到达时间以后也会摧毁自己
  • Sound/FX:相关的特效配置,Start可以用于产生的时候播放,Hit可以用于打中目标以后播放,但都需要手动释放,
    • ProjectileBase已经实现了BeginPlay以后调用Start的效果
    • 在打中目标后已经实现播放Hit的效果
  • BuffData:如果这个技能有buff/debuff可以设置这个类,需要手动调用方法来创建并运行buff类,如下图

BuffRef

这是Buff的实例类,通过SkillRef内部调用CreateBuff方法创建

通过重写BuffInitBuffEnd方法来实现自定义的逻辑,如果是拥有持续效果,那么你可以重写BuffDelta 方法来制作持续调用的逻辑

装备系统

如上图所示,这个是装备信息的配置表,下面简单介绍其中主要参数的作用

  • BodyPart:装备对应的位置
  • Mesh/Mat:用于显示这个装备实体的模型,装备在人物上或者丢弃到世界中的显示
  • Property:如字面意思就是2种属性,其中Adv属性会随着Base的更改而变化

道具系统

道具系统类似于装备系统

  • EffectType:作用类型,本案例只制作了治疗效果和增加魔法的效果
  • Value: 可以设置作用效果的范围值,当然这个不是必须的,具体是下面的道具类内部实现
  • CanBeStacked:如果是否那么一个道具占据背包栏的一格,否则么可以堆放到一起
  • ActionClass:道具类实现逻辑的类,参考Skill的实例实现,具体逻辑可以写到这个类里面

掉落系统

如上图所示,掉落系统的配置放在DataSystem 这个类里面,为什么没有配置表格呢?O(∩_∩)O

当然也可以,你可以自己创建一个类似的表格然后替换其中的逻辑,这个很简单

这个配置信息是一个Map键值对,键对应的是角色的类,值即是掉落信息数组

  • Action:选择装备/道具等的Action名称
  • Type:选择是装备/道具,当然你要是想掉落技能的话理论上也可以,请自由发挥
  • Amount:掉落数量
  • Probability:概率,这个概率计算是在角色创建的时候就完成的,非击杀的时候

任务系统

如上图所示,这是任务表格的配置信息

  • Titile:任务的标题,会显示在任务栏内的抬头标题部分
  • Target:任务的目标
  • DynamicTargetStat:这个会动态的刷新,具体实现的逻辑在下面的TaskClass内
  • Descripion:任务描述
  • Reward:奖励
TaskRef

如上图所示,每接受一个任务以后就会产生一个任务类来作为该任务的观察者(代理)

具体这个任务是干什么的你可以通过实现这个类内的逻辑来管理,同时设置任务目标里的动态信息

其他

怪物刷新

怪物刷新是在场景里放置一个BP_AIGenerator管理类来完成的

  • MaxNum:刷新的最大数量
  • AIClass:设置刷新角色的类,超过1个的时候就随机从中刷新
  • Range:以该管理类的位置为中心的半径内刷新怪物

This is Multiplayer RPG Template Documentation,its about configuration and production method of the main system

This case is based on UE4’s ListerServer built-in LAN online game demo, refer to the work WOW

Based on UE4 version 4.25

There are still some imperfections in the demo, which will be followed up for optimization and repair

Skill system

As shown in the above figure, this figure is the configuration information of the skill table. The important parameters are briefly described below

  • Action: This variable represents the unique ID of this behavior. In principle, it is the same as the RowName of the table. In many logics, you need to use the ID to find all information about the skill, or use the ID to execute the skill
  • Display(struct):3 parameters are used to display the general information of this skill, such as the display of the name or description and icons, which are related to the UMG, of course, you can also customize
  • CastData(struct):This is a spell-casting structure, which mainly sets the method and conditions of the spell-casting
    • Type:For example, the default reading spell casting, continuous casting, attack superimposing casting and other methods
    • Time:The time it takes to cast, 0 means instant
    • Duration:If it is a continuous cast, then this parameter means the lead time
    • KeepAttackStat:After the cast is finished, the normal attack will continue
    • MotionCast:Move cast, if set to No, you must stand cast, once you move, interrupt cast
    • NotTargetCast:Does not require target casting, and is generally used for range skills such as Fire and Rain
  • Mp:Mana cost, Warrior Rage
  • CD:Cooling time
  • MaxRange:Maximum release range, calculated in Unreal Units, 0 means unlimited
  • LearnGold/Level:Prerequisites for learning this skill, 0 means no requirement
  • RangeSelectRange:If it is a skill selected by range, this parameter indicates the radius of the effect
  • TargetType:Target type, place of action or friend
  • SkillClass:Skill class, no need to create manually
SkillRef

This is created by the SkillClass configured in the table, there is no need to manually create it, you only need to make the logic and effects of this class

This class is inherited from BP_BaseSkill, which has some messy blueprint logic, but not all are necessary to use. In this case, you can freely play in many cases. Anyway, after the skill is released, you can control this skill through this class Running the instance

Let’s explain it through the skill class of IceBullet

IceBulletInherit fromProjectileBase,As shown in the figure above, this is the basic parameter type of this class

  • speed: flight speed, depends on ProjectileComponent
  • StopDistance: the distance to stop, because this class is to track the target flight, so there will always be when approaching and reaching the target, a logic will be generated after reaching the target and stopping
  • Duration: Duration, if there is no hit, the time will destroy yourself
  • Sound / FX: related special effects configuration, Start can be used to play when it is generated, Hit can be used to play after hitting the target, but all need to be manually released
       - The effect of calling Start after ProjectileBase has realized BeginPlay
       - The Hit effect has been achieved after hitting the target
  • BuffData: If this skill has buff / debuff, you can set this class, you need to manually call the method to create and run the buff class, as shown below

BuffRef

This is an instance class of Buff, created by internally calling CreateBuff method of SkillRef

By rewriting the BuffInit and BuffEnd methods to achieve custom logic, if it has a continuous effect, then you can rewrite the BuffDelta method to make the logic of continuous calls

EquipSystem

As shown in the figure above, this is the configuration table of the equipment information. The following briefly introduces the role of the main parameters

  • BodyPart: the corresponding position of the equipment
  • Mesh/Mat: Used to display the model of this equipment entity, equipped on the character or discarded into the world
  • Property: literally means 2 kinds of properties, of which Adv property will change with the change of Base

PropSystem

The prop system is similar to the equipment system

  • EffectType:Effect type, this case only made the healing effect and the effect of increasing magic
  • Value: You can set the range value of the effect, of course, this is not necessary, specifically the internal implementation of the following prop class
  • CanBeStacked:If one item occupies one space in the backpack bar, otherwise it can be stacked together
  • ActionClass:Prop class implement logic class, refer to Skill instance implementation, specific logic can be written into this class

DropSystem

As shown in the figure above, the configuration of the drop system is placed in the DataSystem class. Why is there no configuration table? O (∩_∩) O

Of course, you can create a similar table yourself and replace the logic in it. This is very simple.

This configuration information is a Map key-value pair, the key corresponds to the role class, and the value is the drop information array

  • Action:Select the action name of equipment / props, etc.
  • Type:Choose equipment / props. Of course, if you want to drop skills, you can theoretically do it. Please play freely.
  • Amount:number of drops
  • Probability:Probability, this probability calculation is done when the character is created, when not kill

TaskSystem

As shown in the figure above, this is the configuration information of the task table

  • Titile:the title of the task, which will be displayed in the header section of the task bar
  • Target:the target of the task
  • DynamicTargetStat:This will be refreshed dynamically, the specific implementation logic is in the following TaskClass
TaskRef

As shown in the figure above, each time a task is accepted, a task class will be generated as an observer (agent) of the task

Specifically what the task is for, you can manage it by implementing the logic in this class, while setting the dynamic information in the task goal

Other

Monster refresh

Monster refresh is done by placing a BP_AIGenerator management class in the scene

  • MaxNum: the maximum number of refreshes
  • AIClass: Set the class for refreshing the character, if more than one, it will be randomly refreshed from it
  • Range: refresh monsters within a radius centered on the position of the management class