DX学习笔记(二):DX初始化
Direct3D初始化
预备知识
组件对象模型
组件对象模型(Component Object Model,COM):不受DirectX语言束缚,并且向后兼容的技术
- 获得COM接口需要借助特定函数,而不是C++的new
- 删除COM有Release方法,而不是delete
Mirrosoft::WRL::ComPtr
类是Window是下的COM对象的智能指针- 当ComPtr出作用域时,它会自动调用Release方法
1 | //Get: 返回一个指向此底层COM接口的指针,此方法常用于把原始COM接口指针作为参数传给函数 |
1 | //GetAddressOf:返回指向此底层COM接口指针的地址,此函数可以利用函数参数返回COM接口的指针 |
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),具有最小深度值的像素会最终写入后台缓冲
深度缓冲区也是一种纹理,用如下格式来创建
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:占用64位,取其中的32位指定一个浮点型深度缓冲区,另有8位无符号整数分配给模板缓冲区(stencil buffer),并且将该元素映射到[0,255]
- DXGI_FORMAT_D32_FLOAT:指定一个32位浮点型深度缓冲区
- DXGI_FORMAT_D24_UNORM_指定一个无符号的24位深度缓冲区,并将该元素映射到[0,1]区间;另有8位无符号整数分配给模板缓冲区(stencil buffer),并且将该元素映射到[0,255]
- DXGI_FORMAT_D16_UNORM:指定一个16位浮点型深度缓冲区,并将该元素映射到[0,1]区间
资源与描述符
资源->中间层(即描述符)->GPU
描述符
- 一种把送往GPU的资源进行描述的轻量级结构;
- 绘制所需的资源通过描述符绑定到渲染流水线上;
- 为GPU解释资源,如告知Direct3D某个资源如何使用(绑定到流水线的哪个阶段);
- 指定欲绑定资源中的局部数据
常见描述符
- CBV/SRV/UAV :分表表示常量缓冲区(constant buffer view)、着色器资源视图(shader resource view)和无序访问试图(unordered access view)3种资源;
- ***采样器(sampler,亦有译作取样器)***:表示采样器资源(用于纹理)
- RTV:渲染目标视图资源(render target view)
- DSV:深度/模板视图资源(depth/stencil view)
描述符堆:存放某种特定类描述符的内存,可以看作是描述符数组
多重采样
超级采样(SSAA)
超级采样:反走样技术
- 使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲去;
- 3D场景以这种更大的分辨率渲染到后台缓冲区中;
- 当数据要从后台缓冲区调往屏幕显示的时候,会将后台缓冲区按4个像素一组进行解析(降采样,downsample),把放大的采样点数降低回原来采样点数每组用求平均值的方法得到相对平滑的像素颜色
- 实际上是通过软件的方式提升了画面分辨率
- 超级采样是高开销的操作,因为限速处理数量和占用内存大小都增加到了4倍,因此Direct3D支持一种性能和效果折中的反走样技术:多重采样(multisampling),记作MSAA
多重采样(MSAA)
- 多重采样不需要对每个子像素都进行计算
- 而是仅计算一次像素中心的颜色,在基于可视性和覆盖性将得到的颜色信息分享给其子像素
区别
超级采样:图像颜色要根据每一个像素来计算,因此每个子像素都可以各具不的颜色;开销更大但是更精确
多重采样:每个像素只需要计算一次,最后假尼姑得到的颜色数据复制到多边形覆盖的所有可见子像素中
用Direct3D进行多重采样
1 | typedef struct DXGI_SAMPLE_DESC |
根据给定的纹理格式和采样数量,用ID3D12Device::CheckFeatureSupport
方法查询对应的质量级别
- 考虑到多重采样会占用内存资源,又为了保证程序性能等原因,通常会把采样数量设定位 4 或 8
- 如果不希望使用多重采样,可以设置采样数量位1,质量设置为0
功能级别
Direct3D 11开始引用了功能级别(feature level),代码里用 D3D_FEATURE_LEVEL
表示
1 | enum D3D_FEATURE_LEVEL |
- 功能级别为不同级别所支持的功能进行严格界定
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 | HRESULT ID3D12Device::MakeResident(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 | //创建队列智能指针 |
内存分配器
内存分配器:存储命令列表里的命令,执行ID3D12CommandQueue::ExecuteCommandLists
方法时,命令队列就会引用分配器里的命令
1 | virtual HRESULT STDMETHODCALLTYPE CreateCommandAllocator( |
type
:命令列表类型D3D12_COMMAND_LIST_TYPE_DIRECT
:GPU可直接执行的命令D3D12_COMMAND_LIST_TYPE_BUNDLE
:打包的命令列表,一般不用
riid
:适配接口的COM IDppCommandAllocator
:输出指向所建命令分配器的指针
1 | //内存管理指针 |
创建命令列表
1 | //原型 |
- nodeMask:如果只有1个GPU,设置成0;多个GPU用于关联的物理GPU
- type:命令列表类型
- pCommandAllocator:所建命令列表关联的命令分配器,类型必须匹配
- pInitialState:指定命令列表的渲染流水线初始状态,一般可以设置为
nullptr
- riid:待创建的
ID3D12CommandList
接口的COM ID
- ppCommandList:输出指向所建命令列表的指针
小结
- 可以创建多个关联同一个命令分配器的命令列表
- 但是不能同时用他们记录命令
- 其中一个命令列表在记录命令时,必须关闭同一分配器的其他命令列表
- 要保证命令列表中的所有命令都会按顺序连续的添加到命令分配器内
- 当创建或重置一个命令列表时,它会处于“打开 ”状态,所以同时为同一命令列表分配器创建两个命令列表会报错
1 | ID3D12CommandQueue::ExecuteCommandList(C);//把命令添加到命令列表 |
GPU与CPU的同步
刷新命令队列:CPU会等待GPU完成所有命令处理,直到到达指定的***围栏点(fence point)***为止。
创建围栏
1 | HRESULT ID3D12Device::CreateFence(UINT64 InitialValue, |
1 | void D3DApp::FlushCommandQueue() |
资源转换
资源冒险:当GPU的写操作还没有完成或者还没有开始,却开始读取资源的情况
为了解决资源冒险问题,Direct3D设计了一组相关状态,如资源在创建的时候会初一默认状态,直到应用程序通过方法将其转换为另一种状态。
转换资源屏障(transition resource barrier):通过一个API调用来转换多个资源要用到的数组
初始化Direct3D
- 初始化流程
- 用
D3D12CreateDevice
函数创建ID3D12Device
接口实例 - 创建一个
ID3D12Fence
对象,并且查询描述符的大小 - 检测用户设备对
4X MSAA
质量级别的支持情况 - 一次创建命令队列、命令列表分配器和主命令列表
- 描述并创建交换链
- 创建应用程序所需的描述符堆
- 调整后台缓冲区的大小,并为它创建渲染图标视图
- 创建深度/模板缓冲区及与之关联的深度/模板视图
- 设置视口(viewport)和裁剪矩形(scissor rectangle)
创建设备
1 | HRESULT WINAPI D3D12CreateDevice( |
案例
1 | // Try to create hardware device. |
pAdapte
r:使用的显示适配器,如果空,则使用主显示适配器MinimumFeatureLevele
:应用程序需要硬件所支持的最低功能级别,如果适配器不支持此功能级别,则设备创建失败riid
:ID3D12Device
接口的COM IDppDevice
:返回创建的Direct3D 12设备
创建失败的话会尝试创建
WARP
设备
创建围栏
1 | ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, |
检测4X MSAA支持
1 | // Check 4X MSAA quality support for our back buffer format. |
创建命令队列和命令列表
命令队列:
ID3D12CommandQueue
命令分配器:
ID3D12CommandAllocator
命令列表:
ID3D12GraphicsCommandList
1 | //.h申明 |
创建交换链
DXGI_SWAP_CHAIN_DESC
结构体定义
1 | typedef struct DXGI_SWAP_CHAIN_DESC |
- BufferDesc:后台缓冲区的属性,主要是高度、宽度和像素格式
- SampleDesc:多重采样的质量级别以及对每个像素的采样次数
- BufferUsage:如果要将数据渲染到后台缓冲区,则设置为
DXGI_USAGE_RENDER_TARGET_OUTPUT
- BufferCount:缓冲区数量,指定为2即双缓冲
- OutputWindow:渲染窗口的句柄
- Windowed:true则窗口模式运行,否则全屏
- SwapEffect:指定为
DXGI_SWAP_EFFECT_FLIP_DISCARD
- Flags:可选;如果指定为
DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH
则程序切换为全屏时,将选择适用于当前窗口尺寸的显示模式;否则就采用当前桌面的显示模式
DXGI_MODE_DESC
1 | typedef struct DXGI_MODE_DESC |
执行创建交换链
1 | void D3DApp::CreateSwapChain() |
创建描述符堆
创建描述符堆来存储程序中要用到的描述符/视图,本例中需要创建两个描述符堆来存储SwapChainBufferCount
个RTV
,另外一个存储1个DSV
1 | //.h |
创建以后需要通过方法来获得描述符的句柄
1 | D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const |
1 | D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const |
创建渲染目标视图
资源不能与渲染流水线中的阶段直接绑定,所以必须先为资源创建视图(描述符),并将其绑定到流水线阶段
1 | virtual HRESULT STDMETHODCALLTYPE GetBuffer( |
- Buffer:后台缓冲区索引
- riid:COM ID
- ppSurface:返回
ID3D12Resource
接口的指针,即后台缓冲区
调用此方法后会增加计数,所以使用后需要释放,需通过ComPtr
然后获得后台缓冲区创建的渲染目标视图
1 | virtual void STDMETHODCALLTYPE CreateRenderTargetView( |
- pResource:指定用作渲染目标的资源
- pDesc:指向
D3D12_RENDER_TARGET_VIEW_DESC
数组结构体的指针 - DestDescriptor:引用所创建渲染目标视图的描述符句柄
1 | CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart()); |
创建深度/模板缓冲区及其视图
因为深度缓冲区就是一种2D纹理,所以我们通过填写D3D12_RESOURCE_DESC
结构体来描述纹理资源
再用ID3D12Device::CreateCommittedResource
方法来创建它
- D3D12_RESOURCE_DESC
1 | typedef struct D3D12_RESOURCE_DESC |
D3D12_RESOURCE_DIMENSION Dimension:资源的维度
- ```cpp
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的指针
- ```cpp
D3D12_HEAP_PROPERTIES
1 | typedef struct D3D12_HEAP_PROPERTIES |
D3D12_HEAP_TYPE
1 | enum D3D12_HEAP_TYPE |
示例
1 | // Create the depth/stencil buffer and view. |
1 | // 使用资源格式将描述符创建为整个资源的MIP级别0。 |
设置视口
1 | D3D12_VIEWPORT mScreenViewport; |
1 | typedef struct D3D12_VIEWPORT |
填好结构体以后通过函数ID3D12GraphicsCommandList::RSSetViewports
方法来设置视口
示例
1 | mScreenViewport.TopLeftX = 0; |
不能为同一个渲染目标指定多个视口
而多个视口则是一种用于对多个渲染目标同时进行渲染的高级技术
命令列表重置,视口也要重置
设置裁剪矩形
1 | typedef struct tagRECT |
1 | mScissorRect = { 0, 0, mClientWidth, mClientHeight }; |
不能为同一个渲染目标指定多个裁剪矩形。
多裁剪矩形是以各种用于同时对多个渲染目标进行渲染的高级技术
裁剪矩形需要随着命令列表重置而重置
计时与动画
性能计时器
QueryPerformanceCounter
函数来活得性能计时器测量的当前时刻值
1 | __int64 countsPerSec; |
通过如下方式转换为秒
1 | valueInSecs=valueInCounts * mSecondsPercount; |
调用2次
QueryPerformanceCounter
函数得到两个时间戳的相对插值
1 | __int64 A; |
B-A
即可获得执行期间的计数值,或者(B-1)*mSecondsPerCount
获得代码运行期间所花费的秒数
游戏计时器类
1 | class GameTimer |
帧与帧之间的时间间隔
1 | void GameTimer::Tick() |
次Tick
函数被调用在D3DApp::Run
函数
1 | int D3DApp::Run() |
Reset
方法初始化第一帧的数据,因为第一帧没有之前的帧
1 | void GameTimer::Reset() |
总时间
1 | float GameTimer::TotalTime()const |
应用程序框架示例
自己模仿框架的代码
DXApp.h
1 |
|
DXApp.cpp
1 |
|