DX学习笔记(四):绘制几何体
用DirectX绘制几何体
流程简述
顶点与输入布局
- 创建顶点结构体
- 设置输入布局描述
D3D12_INPUT_ELEMENT_DESC
顶点/索引缓冲区
- 创建顶点数据/索引数据
- 创建缓冲区/索引缓冲区
顶点着色/像素着色
- 编写HLSL着色器文件
- 编译着色器
常量缓冲区(上传堆,CPU可写,GPU可读)
- 填充常量缓冲区所需的描述符(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
) - 构造缓冲区对象(单独的类),该类创建常量缓冲区
- 设置缓冲区大小为256B的整数倍
- 创建常量缓冲区视图
- 创建根签名
后续在提交命令列表的时候讲根签名和常量缓冲区绑定到流水线
流水线状态对象 PSO
- 创建PSO对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC
- 设置PSO对象的各类参数,包括输入布局描述符、根签名、PS、VS、光栅器对象等
- 通过
md3dDevice->CreateGraphicsPipelineState(&psoDesc,IID_PPV_ARGS(&mPSO))
创建PSO
构建几何体
- 顶点/索引数据,布局描述
- 创建Geo类,并且设置类内参数,这些参数在填充视口描述的时候需要
- 创建顶点/索引缓冲区
- 在绘制阶段通过命令列表将顶点和索引视图绑定到流水线插槽
- 通过
DrawIndexedInstanced
绘制几何体
刷新(update)
- 根据鼠标信息重新构建观察矩阵(相对坐标的变换)
- 刷新常量缓冲区
Resize
- 重新构建透视投影矩阵
- 然后反馈到Update的常量缓冲区上
世界矩阵随移动/旋转/缩放而改变
观察矩阵随虚拟摄像机的移动/旋转/缩放而改变
投影矩阵随窗口大小调整而改变
刷新的数据通过根签名设置到着色器
顶点与输入布局
- 顶点结构体
1 | struct Vertex |
1 | struct Vertex2 |
- 输入布局描述
1 | typedef struct D3D12_INPUT_LAYOUT_DESC |
1 | typedef struct D3D12_INPUT_ELEMENT_DESC |
- SemanticName:语义,用于将顶点结构体中的元素与顶点着色器输入签名(参数列表)中的元素映射起来
- SemanticIndex:附加到语义的索引。
- Format:顶点元素格式,如
DXGI_FORMAT_R32_FLOAT
1D32位浮点标量 - InputSlot:传递元素所用的输入槽,DirectX12一共16个,索引0-15,一般用同一个输入槽即输入槽0
- AlignedByteOffset:顶点结构体的首地址到某点元素起始地址的偏移量
1 | struct Vertex2 |
- InputSlotClass:一般指定为
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA
,另外一种D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA
用于实现实例化的高级技术 - InstanceDataStepRate:一般设置0,如果采用实例化就用1
- 示例
1 | D3D12_INPUT_ELEMENT_DESC desc1 = |
1 | D3D12_INPUT_ELEMENT_DESC desc2 = |
顶点缓冲区
为了使GPU可以访问顶点数组,就需要把它们放置在缓冲区
的GPU资源(ID2D12Resource)
顶点缓冲区:存储顶点的缓冲区
缓冲区描述:
CD3D12_RESOURCE_DESC
1 | struct CD3DX12_RESOURCE_DESC : public D3D12_RESOURCE_DESC |
此描述继承自D3D_RESOURCE_DESC
,重点实现了几个比较便捷的方法如
1 | static inline CD3DX12_RESOURCE_DESC Buffer( |
其他还有比如CD3DX12_RESOURCE_DESC::Tex1D,CD3DX12_RESOURCE_DESC::2D
等便捷方法
然后通过ID3D12Device::CreateCommittedResource
方法创建ID3D12Resource
对象
- 示例:创建默认缓冲区函数申明
1 | // Create the actual default buffer resource. |
DX12中所有资源都用
ID3D12Resource
接口表示,DX11用各种比如ID3D11Buffers
等表示
为了优化性能,静态几何体的顶点缓冲区置于默认堆中
因为CPU不能向默认堆(D3D12_HEAP_TYPE_DEFAULT
)中的顶点缓冲区写入数据,所以就需要用到一个中介
上传缓冲区
为了将数据从CPU复制到GPU显存中,即将顶点数据从系统内存复制到上传缓冲区,然后再复制到真正的顶点缓冲区中示例:创建默认缓冲区函数实现
1 | Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer( |
- D3D12_SUBRESOURCE_DATA
1 | typedef struct D3D12_SUBRESOURCE_DATA |
- 示例:创建有立方体8个顶点的默认缓冲区
1 | std::array<Vertex, 8> vertices = |
- 顶点缓冲区视图:为了将顶点缓冲区绑定到渲染流水线
1 | typedef struct D3D12_VERTEX_BUFFER_VIEW |
- BufferLocation:顶点缓冲区的虚拟地址,通过
ID3D12Resource::GetGPUVirtualAddress
方法得到此地址 - SizeInBytes:顶点缓冲区大小(字节)
- StrideInBytes:每个顶点元素占用的字节
然后将顶点缓冲区视图和渲染流水线上的一个输入槽绑定
1 | //第一个参数:所用的输入槽,一般用0 |
将顶点缓冲区置入输入槽并不会进行绘制操作,仅仅是为顶点数据送至渲染流水线做好准备
真正绘制顶点的方法
1 | //参数1:每个实例绘制的顶点数 |
索引和索引缓冲区
- 索引缓冲区
与顶点类似,为了使GPU可以访问索引数组,需要将它们放置于GPU的缓冲区资源内,此缓冲区即索引缓冲区
同样用CreateDefaultBuffer
方法创建索引缓冲区
索引缓冲区用到结构体D3D12_INDEX_BUFFER_VIEW
描述
1 | typedef struct D3D12_INDEX_BUFFER_VIEW |
同顶点缓冲区,也需要绑定到渲染流水线,通过ID3D12GraphicsCommandList::IASetIndexBuffer
方法将索引缓冲区绑定到输入装配器阶段
1 | std::array<std::uint16_t, 36> indices = |
使用索引时,要用
ID3D12GraphicsCommandList::DrawIndexedInstanced
代替DrawInstanced
方法
1 | //第4个参数:读取顶点之前,要为每个索引都加上的整数值 |
将多个顶点缓冲区和索引缓冲区合并以后会需要用到第4个参数
1 | mCommandList->DrawIndexedInstanced(numOfBox, 1, 0, 0, 0); |
顶点着色器示例
- 高级着色器语言:HLSL
示例
1 | cbuffer cbPerObject:register(b0) |
HLSL没有指针和引用,用out
表示输出参数
HLSL所有函数都是lnline函数
参数POSITION
和COLOR
语义将元素映射到顶点着色器对应的输入参数;同理输出参数的语义将参数映射到下一个阶段(几何着色器或者像素着色器)对应的输入参数;
SV_POSTION
的SV
代表系统值,此数据存有齐次裁剪空间顶点信息,指定了此语义以后使GPU进行如裁剪、深度测试等处理时,实现其他属性无法介入的有关运算
任何没有系统修饰的参数,都可以根据需求以合法的语义修饰
可以把函数的参数封装成结构体,下面是另一种实现方式
1 | struct VertexIn |
如果没有使用
几何着色器
,那么顶点着色器必须用SV_POSITION
来输出顶点在齐次裁剪空间的位置如果使用了,可以把得到齐次裁剪空间位置的工作交给它来处理
顶点着色器/几何着色器无法完成透视除法,此阶段只能完成投影矩阵运算,透视除法由后续硬件执行
布局描述符与签名匹配问题
ID3D12Device::CreateGraphicsPipelineState
函数时,需要指定布局描述符和顶点着色器,这就涉及到参数匹配问题
- 不匹配
1 | struct Vertex |
- 匹配
1 | struct Vertex |
- 匹配
1 | struct Vertex |
像素着色器示例
光栅化阶段先对顶点着色器进行插值计算,然后把数据传递至像素着色器作为输出
像素着色器针对每一个像素片段而运行的函数,但是部分像素片段不会传入或者留存在后台缓冲区,会在深度测试等情况被丢弃掉;即像素是最终写入后台缓冲区的数据,像素片段是候选像素
示例
1 | cbuffer cbPerObject:register(b0) |
参数列表后的SV_TARGET
表示该返回值的类型樱岛与渲染目标格式相匹配(render target format
)1
同样可以利用输入输出结构体来重写上述代码
1 | struct VertexIn |
常量缓冲区
创建常量缓冲区
常量缓冲区也是一种GPU资源ID3D12Resource
1 | cbuffer cbPerObject:register(b0) |
此代码中的cbuffer
对象(常量缓冲区)的名称就是cbPerObject
,存储一个4x4矩阵gWorldViewProj
此矩阵表示把一个点从局部空间变换到齐次裁剪空间所用到的由世界、视图和投影3种变换组合而成的矩阵
与顶点和索引缓冲区不同,从常量缓冲区由CPU每帧更新顶一次
常量缓冲区创建到上传堆而非默认堆,使得我们从CPU端更新常量
常量缓冲区的硬件有特别要求,大小必须为256B的整数倍
示例
1 | struct ObjectConstants |
mUploadCBuffer
存储了一个ObjectConstants
类型的常量缓冲区数组
绘制物体时,只要将常量缓冲区视图CBV
绑定到那个存有物体相应常量缓冲区的子区域即可
- 着色器模型5.1
1 | struct ObjectConstants |
常量缓冲区的数据元素被定义在一个单独的结构体中,随后用这个结构来创建一个常量缓冲区,然后就可以在着色器里访问常量缓冲区中的各个字段
1 | uint index = gObjConstants.matIndex; |
着色器模型定义了HLSL的编写规范,确定了其内置函数、着色器属性等一切语言元素
更新常量缓冲区
常量缓冲区是用D3D12_HEAP_TYPE_UPLOAD
类型创建的,所以可以用CPU为常量缓冲区更新数据
1 | Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer; |
1 | memcpy(mMappedData,&data,dataSizeInBytes);//用此函数将数据从系统内存复制到常量缓冲区 |
等更新完成,然后在释放映射内存之前对其进行Unmap(取消映射)
操作
1 | if(mUploadBuffer != nullptr) |
Unmap
的第一个参数是子资源索引,第二个是指向D3D12_RANGE
结构体的指针,描述取消映射的内存范围,如空则整个资源映射。
上传缓冲区辅助函数
- UploadBuffer类
1 | template<typename T> |
世界矩阵随移动/旋转/缩放而改变
观察矩阵随虚拟摄像机的移动/旋转/缩放而改变
投影矩阵随窗口大小调整而改变
- OnMouseMove
1 | void BoxApp::OnMouseMove(WPARAM btnState, int x, int y) |
- Update
1 | void BoxApp::Update(const GameTimer& gt) |
常量缓冲区描述符
常量缓冲区描述符都存放在以D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
类型的描述符堆里,这个堆混合存储了常量缓冲区、着色器资源和无序访问描述符
1 | D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc; |
在创建着色器程序访问的描述符时,需要把Flags指定为
DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
然后通过如下代码创建常量缓冲区
1 | void BoxApp::BuildConstantBuffers() |
根签名和描述符表
- 根签名
1 | //将纹理资源绑定到纹理寄存器槽0 |
register(*#)
中的*
表示寄存器传递的资源类型。t:着色器资源视图;
s:采样器
u:无序访问视图
b:常量缓冲区视图
在执行绘制命令之前,那些应用程序将绑定到渲染流水线上的资源,它们会被映射到着色器的对应输入寄存器。
根签名一定要与使用它的着色器相兼容,在创建流水线状态对象时会对此进行验证
不同的绘制调用可能会用到一组不同的着色器程序,意味着要用到不同的根签名
在Direct3D中,根签名由ID3D12RootSignature
接口表示,并用一组描述绘制调用过程中着色器所需资源的根参数
定义而成。
根参数可以是根常量
、根描述符
或者描述符表
如果把着色器程序看成是一个大函数,那顶点数据和常量数据就是从CPU传入着色器函数的参数,而根签名就好比这些参数的函数签名。所以根签名其实就是将着色器需要用到的数据绑定到对应的寄存器槽上,供着色器访问。
示例
1 | oid BoxApp::BuildRootSignature() |
1 | // Root parameter can be a table, root descriptor or root constants. |
上述代码创建了一个根参数,目的是将含有一个CBV
的描述符表绑定到常量缓冲区寄存器0,即HLSL
代码中的register(b0)
根签名至定义了应用程序要绑定到渲染流水线的资源,没有真正的执行任何资源绑定操作
只有通过命令列表设置根签名,才可以用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable
方法令描述符表与渲染流水线绑定
1 | mCommandList->SetGraphicsRootDescriptorTable(0, //将根参数按此索引进行设置 |
下列代码先将根签名和CBV堆设置到命令列表上,并随后通过设置描述符表来指定我们希望绑定到渲染流水线的资源
1 | mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); |
·
编译着色器
着色器程序必须先被编译为一种可移植的字节码,接下来图形驱动程序将获取这些字节码,并将其重新便以为针对当前系统GPU所优化的本地指令[ATI1]
可以用如下函数在运行期间对着色器进行编译
1 | HRESULT D3DCompileFromFile( |
- ID3DBlob
一段普通的内存块,有2个接口方法
LPVOID GetBufferPointer
:返回对象中数据的void*
类型的指针,使用之前需要转换为适当类型SIZE_T GetBufferSize
:返回缓冲区字节大小
- 运行时编译着色器函数
1 | ComPtr<ID3DBlob> CompileShader( |
离线编译
离线编译的原因
- 对于复杂的着色器,编译耗时太长,因此借助离线编译即可缩短应用程序的加载时间
- 以便在早于运行时的构建处理期间提早发现编译错误
- 对于Windows9应用商店的应用而言,必须采取离线编译这种方式
一般用
.cso
为已编译的着色器对象的扩展名
使用DirectX自带的FXC
命令行编译工具编译
- 如用CMD输入代码编译
1 | C:\Program Files (x86)\Windows Kits\10\bin\x86>fxc.exe C:\Users\Administrator\Desktop\color.hlsl /T vs_5_0 /Fo "color.cso" /E "VS" |
成功输出color.cso
文件
compilation object save succeeded; see C:\Program Files (x86)\Windows Kits\10\bin\x86\color.cso
- 常用的编译参数
参数 | 描述 |
---|---|
/Od | 禁用优化(利于调试) |
/Zi | 开启调试信息 |
/T “string” | 着色器模型版本,如输入vs_5_0 |
/E “string” | 着色器入口,如案例中的VS ,PS |
/Fo “string” | 经过编译的着色器对象字节码 |
/Fc “string” | 输出一个着色器汇编语言清单(调试、查阅细节) |
其他清单可以参考微软SDK文档
光栅器状态
光栅器状态只接受配置,非可编程,由结构体D3D12_RASTERIZER_DESC
表示
1 | typedef struct D3D12_RASTERIZER_DESC |
FillMode:默认是实体渲染,如果用D3D12_FILL_MODE_WIREFRAME
则是线框渲染
CullMode:默认剔除背面,D3D12_CULL_MODE_NONE
不剔除,D3D12_CULL_MODE_FRONT
剔除正面
FrontCounterClockwise:默认false,根据观察视角,将定点顺序为顺时针方向的三角形看作正面;如true则相反
示例
1 | CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT); |
CD3DX12_RASTERIZER_DESC
是扩展自D3DX12_RASTERIZER_DESC
的结构,添加了一些辅助构造函数的工具类,如接受CD3DX12_DEFAULT
参数的构造函数。
CD3DX12_DEFAULT
是一个哑类型(dummy),将需要被初始化的成员重载为默认值
1 | struct CD3DX12_DEFAULT {}; |
流水线状态对象
ID3D12PipelineState
表示流水线状态对象(PSO)
结构体
1 | typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC |
示例
1 | D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc; |
PSO的验证和创建过于耗时,所以一般在初始化期间就生成PSO
视口和裁剪矩形等属性独立于PSO
用不同的PSO绘制不同的物体
1 | mCommandList->Reset(mDirectCmdListAlloc.Get(),mPSO1.Get()); |
几何图形辅助结构体
此结构体定义了MeshGeometry中存储的单个几何体
1 | struct SubmeshGeometry |
1 | struct MeshGeometry |
绘制Box
1 |
|