#利用Direct3D绘制几何体c++
学习目标
- 探索用于定义、存储和绘制几何体数据的Direct接口和方法
- 学习编写简单的顶点着色器和像素着色器
- 了解如何用渲染流水线状态对象来配置渲染流水线
- 理解怎样建立常量缓冲区数据。并将其绑定到渲染流水线上
- 掌握根签名的用法
##6.1 顶点与输入布局 由5.5.1节可知,除了空间位置,Direct3D的顶点还能够存储不少其余的属性数据。为了构建自定义的顶点格式,咱们首先要建立一个结构体来容纳选定的顶点数据。好比:编程
//由位置和颜色信息组成的顶点结构体 typedef struct Vertex1 { XMFLOAT3 Pos; XMFLOAT4 Color; }; //由位置、法向量以及两组2D纹理坐标组成的顶点结构体 typedef struct Vertex2 { XMFLOAT3 Pos; XMFLOAT3 Normal; XMFLOAT2 Tex0; XMFLOAT2 Tex1; };
定义完顶点结构体以后,咱们还须要向Direct3D提供该顶点结构体的描述,使他了解应该要怎样处理顶点结构体中的每个成员。这种描述称为输入布局描述,咱们能够用结构体D3D12_INPUT_LAYOUT_DESC来表示输入布局描述:数组
typedef struct D3D12_INPUT_LAYOUT_DESC { const D3D12_INPUT_ELEMENT_DESC * pInputElementDesc; //D3D12_INPUT_ELEMENT_DESC元素构成的数组 UINT NumElements; //数组元素数量 }D3D12_INPUT_LAYOUT_DESC;
D3D12_INPUT_ELEMENT_DESC数组中的元素依次描述了顶点结构体中对应的成员,若是某一个顶点结构体有两个成员,那么与之对应的D3D12_INPUT_ELEMETN_DESC数组也将会有两个元素。D3D12_INPUT_ELEMENT_DESC结构体的定义以下:app
typedef struct D3D12_INPUT_ELEMENT_DESC { LPCSTR SemanticName; //语义,传达该元素的用途 UINT SemanticIndex; //附加到语义上的索引 DXGI_FORMAT Format; //指定顶点元素的格式(即数据类型) UINT InputSlot; //指定传递元素所使用的输入槽 UINT AlignedByteOffset; //从C++顶点结构体的首地址到其中某点元素起始地址的偏移量 D3D12_INPUT_CLASSIFICATION InputSlotClass; //暂时指定为D3D12_INPUT_CALSSIFICATION_PER_VERTEX_DATA UINT InstanceDataSetpRate; //暂时指定为0 }D3D12_INPUT_ELEMETN_DESC;
下面是以本节开头的Vertex1和Vertex2这两个顶点结构体的对应的输入布局描述:ide
D3D12_INPUT_ELEMETN_DESC desc1[] = { {"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0 }, {"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,12,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0} }; D3D12_INPUT_ELEMENT_DESC desc2[] = { {"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0}, {"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0}, {"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,24,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0}, {"TEXCOORD",1,DXGI_FORMAT_R32G32_FLOAT,0,32,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0} };
##6.2顶点缓冲区 为了使GPU能够访问顶点数组,咱们须要把顶点数组放置在称为缓冲区的GPU资源里,咱们把存储顶点的缓冲区称为顶点缓冲区。函数
咱们要先经过填写D3D12_RESOURCE_DESC结构体来描述缓冲区资源,接着在调用ID3D12Device::CreateCommittedResource方法来建立ID3D12Resource对象。固然,咱们也可使用D3D12_Resource_Desc的派生类CD3DX12_RESOURCE_DESC来建立ID3d12Resource对象工具
对于静态几何体(每一帧都不会发生改变的几何体)而言,咱们会将它的顶点缓冲区放置在默认堆中来优化性能。由于静态几何体的顶点缓冲区初始化完成以后,只有GPU须要从顶点缓冲区中读取数据,因此能够直接将该顶点缓冲区放在默认堆中。可是,若是CPU不能向默认堆中的顶点缓冲区写入数据,那么咱们要怎样才能够初始化该顶点缓冲区呢?布局
解答:咱们须要使用D3D12_HEAP_TYPE_UPLOAD这种堆类型来建立一个处于中介位置的上传缓冲区资源,而后咱们就能够把顶点数据从系统内存复制到上传缓冲区中,而后把顶点数据从上传缓冲区复制到真正的顶点缓冲区中性能
咱们在d3dUtil文件中构建了相关的工具函数,以免在每次使用默认缓冲区时要重复的工做学习
Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer( ID3D12Device * device, ID3D12GraphicsCommandList * cmdList, const void * initData, UINT64 byteSize, Microsoft::WRL::ComPtr<ID3D12Resource> uploadBuffer ) { ComPtr<ID3D12Resource> defaultBuffer; //建立实际的默认缓冲区资源 ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteSize), D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(defaultBuffer.GetAddressOf()))); //建立一个处于中介位置的上传堆 ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteSize), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(uploadBuffer.GetAddressOf()))); //描述咱们但愿复制到默认缓冲区中的数据 D3D12_SUBRESOURCE_DATA subResourceData = {}; subResourceData.pData = initData; subResourceData.RowPitch = byteSize; subResourceData.SlicePitch = subResourceData.RowPitch; //转换默认缓冲区的状态 cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_GENERIC_READ)); //将上传堆的数据复制到默认缓冲区中 UpdateSubresources(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData); //将默认缓冲区的状态转变为普通状态 cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_COMMON)); //返回默认缓冲区 return defaultBuffer; }
下面的代码展现了如何建立存有立方体八个顶点的默认缓冲区,并为每个顶点都分别赋予了不一样的颜色
Vertex vertices[] = { Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }), Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }), Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }), Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }), Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }), Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }), Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }), Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }), Vertex({ XMFLOAT3(0, 0, +1.0f), XMFLOAT4(Colors::Red) }) }; const UINT64 vbByteSize = 8 * sizeof(Vertex); ComPtr<ID3D12Resource> VertexBufferGPU = nullptr; ComPtr<ID3D12Resource> VertexBufferUploader = nullptr; VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(), mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
为了将顶点缓冲区绑定到渲染流水线上,咱们还要为顶点缓冲区建立一个顶点缓冲区视图,不过咱们没必要为顶点缓冲区视图建立描述符堆,顶点缓冲区视图是由结构体D3D12_BUFFER_VIEW表示的:
typedef struct D3D12_VERTEX_BUFFER_VIEW { D3D12_GPU_VIRTUAL_ADDRESS BufferLocation; //顶点缓冲区的虚拟地址 UINT SizeInByte; //顶点缓冲区的大小 UINT StrideInByte; //每一个顶点元素占用的字节数 }D3D12_VERTEX_BUFFER_VIEW;
在顶点缓冲区以及其对应的视图建立完成以后,咱们就能够将它和渲染流水线上的一个输入槽绑定了。这样咱们就能够向流水线中的输入装配阶段传递顶点数据了。此操做能够有如下函数实现:
void ID3D12GraphicsCommandList::IASetVertexBuffers( UINT StartSlot, UINT NumViews, const D3D12_VERTEX_BUFFER_VIEW * pViews );
将顶点缓冲区设置到输入槽上并不会对其执行真正的绘制操做,而是仅仅为顶点数据传送到渲染流水线上作准备,咱们经过ID3D12GraphicsCommanList::DrawInstanced方法才能够真正地绘制顶点:
void ID3D12GraphicsCommandList::DrawInstanced( UINT VertexCountPerInstance, //每一个实例要绘制的顶点数量 UINT InstanceCount, //暂时设置为1 UINT StartVertexLocation, //指定顶点缓冲区内第一个被绘制的顶点的索引 UINT StartInstanceLoaction //暂时设置为0 );
##6.3 索引和索引缓冲区 和顶点类似,为了使GPU能够访问索引数组,咱们须要把索引反之放置在GPU的缓冲区资源你(ID3D12Resource)中,存储索引的缓冲区成为索引缓冲区,咱们也可使用d3dUtil::CreateDefaultBuffer函数来建立索引缓冲区。
为了使索引缓冲区和渲染流水线相互绑定,咱们须要为索引缓冲区建立索引缓冲区视图,和顶点缓冲区视图同样,咱们不须要为索引缓冲区视图建立描述符堆,索引缓冲区视图由结构体D3D12_INDEX_BUFFER_VIEW表示:
typedef struct D3D12_INDEX_BUFFER_VIEW { D3D12_GPU_VIRTUAL_ADDRESS BufferLoaction; //索引缓冲区的虚拟地址 UINT SizeInByte; //索引缓冲区的大小 DXGI_FORMAT Format; //索引的格式 }D3D12_INDEX_BUFFER_VIEW;
和顶点缓冲区类似,在使用以前,咱们要使用ID3D12GraphicsCommandList::IASetIndexBuffer函数来将索引缓冲区绑定到输入装配阶段。最后,咱们要使用ID3D12GraphicsCommandList::DrawIndexedInstanced方法来绘制。
void ID3D12GraphicsCommandList::DrawIndexedInstanced( UINT IndexCountPerInstance, //每一个实例须要绘制的顶点数量 UINT InstanceCount, //暂时设置为1 UINT StartIndexLoaction, //指向索引缓冲区中的某一个元素,该元素为起始索引 int BaseVertexLoaction, //为每个索引加上这个整数值 UINT StartInstanceLoaction //暂时设置为0 );
##6.4 顶点着色器示例 如下代码实现的是一个简单的顶点着色器(vertex shader):
cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; }; void VS(float3 iPosL : POSITION, float4 iColor:COLOR, out float4 oPosH : SV_POSITION, out float4 oColor : COLOR ) { //把顶点变换到齐次裁剪空间 oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj); //直接将顶点的颜色信息输出到像素着色器中 oColor = iColor; }
在Driect3D中,编写着色器的语言为高级着色语言(High Level Shading Language),其语法和c++十分类似。通常状况下,着色器一般要编写在以.hlsl为扩展名的文本文件中。
顶点着色器就是上面那个名为VS的函数,上述顶点着色器有四个参数,前面两个为输入参数,后面两个为输出参数,由于HLSL没有引用和指针的概念,因此须要借助结构体或是多个输出参数才能够返回多个数值。
前两个输入参数分别对应绘制立方体时自定义的顶点结构体中的两个数据成员,也构成了顶点着色器的输入签名,参数语义“POSITION”和“COLOR”用于将顶点结构体的元素映射到顶点着色器的输入签名中。输出参数也有各自的语义,输出参数会根据语义,将顶点着色器的输出映射到下一处理阶段(几何着色器或者像素着色器)中,这里有个“SV_POSITION”语义比较特殊,由于它所修饰顶点着色器输出元素存有齐次裁剪空间中的位置信息。
补充:内置函数mul用于计算向量和矩阵之间的乘法,也能够用于矩阵和矩阵之间的乘法
下面咱们将把顶点着色器的的返回类型和输入签名用结构体替换,以免出现过程的参数列表。即把上述顶点着色器改写成另外一种等价实现:
cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; }; struct VertexIn { float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexOut { float4 PosH : POSITION; float4 Color : COLOR; }; VertexOut VS(VertexIn vIn) { VertexOut vOut; //将顶点数据从局部空间变换到齐次裁剪空间 vOut.PosH = mul(float4(vIn, 1.0f), gWorldViewProj); //直接把顶点颜色做为输出 vOut.Color = vIn.Color; return vOut; }
注意:若是没有使用几何着色器(十二章介绍),那么顶点着色器必须使用SV_POSITION语义输出顶点在齐次裁剪空间中的位置,由于硬件但愿得到顶点在齐次裁剪空间中的坐标,若是使用了几何着色器,那么能够把输出顶点在齐次裁剪空间中的坐标的任务交给几何着色器来处理
链接输入布局描述符和输入签名
略(该小节主要介绍输入的顶点数据和顶点着色器指望的输入不符合的状况)
像素着色器示例
为了计算出三角形内的每个像素的属性,咱们会再光栅化阶段对顶点着色器(或是几何着色器)输出的顶点属性进行插值,而后这些插值数据会做为像素着色器的输入。
像素着色器和顶点着色器类似,后者是针对每个顶点而运行的函数,而前者是针对每个像素片断而运行的函数。只要为像素着色器指定了输入数,它就会为像素片断计算出一个对应的颜色。不过输入像素着色器的片断那不必定会被传入或留存在后台缓冲区中,可能会在进入后台缓冲区以前被裁剪掉了,或者是没有经过深度/模板测试而被丢弃。
下面是一段像素着色器代码,由于要和上一节的顶点着色器相呼应,因此这里也会把顶点着色器的代码一块儿给出来
cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; } void VS(float3 iPosL:POSITION, float4 iColor:COLOR, out float4 oPosL:SV_POSITION, out float4 oColor : COLOR ) { //将顶点变换到齐次裁剪空间 oPosL = mul(float4(iPosL, 1.0f), gWorldViewProj); //直接将顶点颜色传递到像素着色器 oColor = iColor; } float4 PS(flaot4 posH : SV_POSITION, float4 color : COLOR) : SV_Target { return color; }
在上面的示例中,像素着色器只是简单的返回了插值颜色数据,能够发现,像素着色器的输入和顶点着色器的输出是精确匹配的,这是必需要知足的一点。而位于像素着色器参数列表后面的语义SV_TARGE则表示该返回值的类型和渲染目标格式相互匹配(该输出会被存到渲染目标之中)
和顶点着色器同样,咱们能够利用输入/输出结构体重写像素着色器,以下:
cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; } struct VertexIn { float3 Pos : POSITION; flaot4 Color : COLOR; }; struct VertexOut { float4 PosH : SV_POSITION; float4 Color : COLOR; }; VertexOut VS(VertexIn vIn) { VertexOut vOut; //将顶点坐标从局部空间转换到齐次裁剪空间 vOut.PosH = mul(flaot4(vIn.Pos, 1.0f), gWorldViewProj); //直接将输入颜色输出到像素着色器中 vOut.Color = vIn.Color; return vOut; } flaot4 PS(VertexIn vIn):SV_Target { return vIn.Color; }
##6.6 常量缓冲区 常量缓冲区也是一种GPU资源(ID3D12Resource),其数据内容能够给着色器程序使用,就像咱们即将学习到的纹理等其余资源同样,他们均可以被着色器程序使用。
和顶点缓冲区不一样的是,常量缓冲区由CPU每帧更新一次,因此咱们会把常量缓冲区建立到一个上传堆中而不是默认堆(只有GPU能访问的堆,CPU没法对其进行写入)中。同时,常量缓冲区对硬件也有特别的要求,即常量缓冲区的大小必须是硬件最小分配空间(256B)的整数倍
因为咱们常常要使用多个相同类型的常量缓冲区,因此下面的代码将展现如何建立一个缓冲区资源,并利用该缓冲区来存储NumElements个常量缓冲区:
struct ObjectConstants { DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4(); }; UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)); ComPtr<ID3D12Resource> mUploadCBuffer; md3dDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*NumElement), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mUploadCBuffer) )
工具函数d3dUtil::CalcConstantBufferByteSize()会进行适当的计算,使缓冲区的大小凑整为硬件的最小分配空间的整数倍。(函数内部具体实现不解释)
随着Direct3D一块儿推出的是着色器模型(Shader Model)5.1,其中新引进了一条能够用于定义常量缓冲区的HLSL语法,它的使用方法以下:
struct ObjectConstances { flaot4x4 gWorldViewProj; }; ConstantBuffer<ObjectConstances> gObjectConstants : register(b0);
咱们在前面的实例中使用的都是着色器模型5.0的标准,接下来咱们会尽量的使用着色器模型5.1的标准,(5.1暂时不支持Driect11)
##6.6.2 更新常量缓冲区 因为常量缓冲区是使用D3D12_HEAP_TYPE_UPLOAD这种类型建立的,因此咱们能够经过CPU来更新数据,为此,咱们须要获取指向欲更新数据的指针,可使用Map方法获取:
ComPtr<ID3D12Resource> mUploadBuffer; BYTE* mMappedData = nullptr; mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
Map方法的三个参数的意义分别是:
- 指定了欲映射的子资源的索引,对于缓冲区来讲,它自身即是惟一的子资源,因此咱们能够把这个参数设置为0;
- 第二个参数是一个可选项,用于指定内存的映射范围,若是该参数指定为空,则对整个资源进行映射;
- 返回待映射资源数据的目标内存块
当常量缓冲区更新完成以后,咱们应该在释放映射内存以前对其进行Unmap(取消映射)操做。
if(mUploadBuffer != nullptr) { mUploadBuffer->Unmap(0,nullptr); }
##6.6.3 上传缓冲区辅助函数 为了使上传缓冲区的相关处理工做更加轻松,咱们在UploadBuffer.h文件中定义了下面这个类,它会替咱们实现上传缓冲区资源的构造和析构函数,处理资源的映射和取消映射操做,还提供了CopyData方法来更新缓冲区中的特定元素。(这个类不是仅仅针对常量缓冲区,也能够用来管理各类类型的上传缓冲区)。
template<typename T> class UploadBuffer { public: UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :mIsConStantBuffer(isConstantBuffer) { mElementByteSize = sizeof(T); //若是是常量缓冲区。将缓冲区的大小设置为硬件最小分配空间的整数倍 if (isConstantBuffer) { mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T)); } ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer( mElementByteSize*elementCount), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mUploadBuffer) )); ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData))); } UploadBuffer(const UploadBuffer& rhs) = delete; UploadBuffer& operator=(const UploadBuffer& rhs) = delete; ~UploadBuffer() { if (mUploadBuffer != nullptr) { mUploadBuffer->Unmap(0, nullptr); } mMappedData = nullptr; } ID3D12Resource* Resource()const { return mUploadBuffer.Get(); } void CopyData(int elemetnIndex, const T& data) { memcpy(&mMappedData[elemetnIndex*mElementByteSize], &data, sizeof(T)); } private: Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer; BYTE* mMappedData = nullptr; UINT mElementByteSize = 0; bool mIsConStantBuffer = false; };
题外话:通常来讲,物体的世界矩阵会根据移动/旋转/缩放而改变,观察矩阵会根据虚拟摄像机的移动/旋转而改变,投影矩阵会根据窗口大小的调整而改变。
##6.6.4 常量缓冲区描述符 到目前为止,咱们已经介绍了渲染目标,深度/模板缓冲区,顶点缓冲区以及索引缓冲区这几种资源视图(描述符)的使用方法,接下来咱们将介绍如何利用描述符将常量缓冲区绑定到渲染流水线中,
由于常量缓冲区须要使用D3D12_DESCRIPTOR_HEAP_CBV_SRV_UAV类型所建立的描述符堆,这种堆内能够存储常量缓冲区视图,着色器资源视图以及无序访问视图(unordered access),为了存放这些描述符,咱们须要建立如下类型的描述符堆
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc; cbvHeapDesc.NumDescriptors = 1; cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; cbvHeapDesc.NodeMask = 0; cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; ComPtr<ID3D12DescriptorHeap> mCbvHeap; md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap));
而后经过填写D3D12_CONSTANT_BUFFER_VIEW_DESC实例,再调用ID3D12Device::CreateConstantBufferView方法即可以建立常量缓冲区视图:
//绘制物体所用对象的常量数据 struct ObjectConstant { XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4(); }; //建立一个存储绘制n个物体所需常量数据的常量缓冲区 std::unique_ptr<UploadBuffer<ObjectConstant>> mObjectCB = nullptr; mObjectCB = std::make_unique<UploadBuffer<ObjectConstant>>(md3dDevice.Get(), n, true); UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstant)); //缓冲区的起始地址(索引为0的常量缓冲区地址) D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress(); //偏移到常量缓冲区中绘制第i个物体所须要的常量数据 int boxCBufferIndex = i; cbAddress += objCBByteSize * boxCBufferIndex; //绑定到HLSL常量缓冲区结构体的常量缓冲区资源子集 D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc; cbvDesc.BufferLocation = cbAddress; cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstant)); md3dDevice->CreateConstantBufferView(&cbvDesc, mCbvHeap->GetCPUDescriptorHandleForHeapStart());
根签名和描述符表
在绘制调用开始以前,咱们要把不一样类型的资源绑定到特定的寄存器槽上,以供着色器程序访问。好比说,前文的顶点着色器和像素着色器须要的就是一个绑定到寄存器b0的常量缓冲区,在后续的章节中,咱们会使用到这两种着色器更高级的配置方法,以使多个常量缓冲区、纹理和采样器均可以和各自的寄存器槽相互绑定
根签名:在执行绘制命令以前,根签名必定要为着色器提供其执行期间所须要绑定到渲染流水线的全部资源,在建立流水线状态对象(pipeline state object)时会对此进行验证,不一样的绘制调用可能须要不一样的着色器程序,这样意味着要使用不一样的根签名。
在Direct3D中,根签名由ID3DRootSignature接口表示,并经过一组根参数(用以描述绘制调用过程当中着色器所需的资源)定义而成,根参数能够是根常量、根描述符、或者描述符表。下面的代码将建立一个根签名,他的根参数为描述符表(描述符表是描述符堆一块连续区域):
CD3DX12_ROOT_PARAMETER slotRootParameter[1]; //建立一个只存有一个CBV的描述符表 CD3DX12_DESCRIPTOR_RANGE cbvTable; cbvTable.Init( D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, //表中描述符数量 0 //这段描述符区域绑定的目标寄存器槽编号 ); slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable); //根签名由一组根参数组成 CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc(1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); //建立一个仅含有一个槽位的根签名 ComPtr<ID3D12RootSignature> mRootSignature = nullptr; ComPtr<ID3DBlob> serializedRootSig = nullptr; ComPtr<ID3DBlob> errorBlod = nullptr; HRESULT hr = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, serializedRootSig.GetAddressOf(), errorBlod.GetAddressOf()); ThrowIfFailed(md3dDevice->CreateRootSignature( 0, serializedRootSig->GetBufferPointer(), serializedRootSig->GetBufferSize(), IID_PPV_ARGS(&mRootSignature)));
咱们将在第七章对CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE这两个结构体进行详细的说明,在这里只须要理解如下代码便可:
CD3DX12_ROOT_PARAMETER slotRootParameter[1]; //建立一个只存有一个CBV的描述符表 CD3DX12_DESCRIPTOR_RANGE cbvTable; cbvTable.Init( D3D12·_DESCRIPTOR_RANGE_TYPE_CBV, 1, //表中描述符数量 0 //这段描述符区域绑定的目标寄存器槽编号 ); slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);
这段代码建立了一个根参数,目的是将含有一个CBV的描述符表绑定到常量缓冲区寄存器0
根签名只定义了应用程序要绑定的渲染流水线的资源,不过没有真正执行任何资源绑定操做,只有率先经过命令列表设置好根签名,而后使用ID3D12GraphicsCommandList::SetGraphicRootDescriptorTable方法令描述符表和渲染流水线相互绑定
下列代码先将根签名和CBV设置到命令列表中,而后经过设置描述符表来指定咱们但愿绑定到渲染流水线的资源:
mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() }; mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); //偏移到这次绘制调用所需的CBV处 CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart()); cbv.Offset(cbvIndex, mCbvSruUavDescriptorSize); mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
##6.7 编译着色器 在Direct3D中,着色器程序必需要先被编译为一种可移植的字节码,接下来,图形驱动程序将获取这些字节码,并将这些字节码从新编译为针对当前系统GPU所优化的本地指令,咱们在运行期间可使用如下函数对着色器程序进行编译:
HRESULT D3DCompileFormFile( LPCWSTR pFlieName, const D3D_SHADER_MACRO * pDefines, ID3DInclude * pInclude, LPCSTR pEntrypoint, LPCSTR pTarget, UINT Falgs1, UINT Flags2, ID3DBlob ** ppCode, ID3DBlob ** ppErrorMsgs );
为了可以输出编译着色器的错误信息,咱们在d3dUtil文件中实现了下列辅助函数在运行时编译着色器:
ComPtr<ID3DBlob> d3dUtil::CompileShader( const std::wstring& filename, const D3D_SHADER_MACRO* defines, const std::string& entrypoint, const std::string& target ) { //若是处于调试状态,则使用调试标志 UINT compileFalgs = 0; #if defined(DEBUG) || defined(_DEBUG) compileFalgs = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif HRESULT hr = S_OK; ComPtr<ID3DBlob> byteCode = nullptr; ComPtr<ID3DBlob> errors = nullptr; hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint.c_str(), target.c_str(), compileFalgs, 0, &byteCode, &errors); //将错误信息输出到调试窗口 if (errors != nullptr) { OutputDebugStringA((char*)errors->GetBufferPointer()); } ThrowIfFailed(hr); return byteCode; }
如下是调用此函数的实例:
ComPtr<ID3DBlob> mvsByteCode = nullptr; ComPtr<ID3DBlob> mpsByteCode = nullptr; mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color,hlsl", nullptr, "VS", "vs_5_1"); mpsByteCode = d3dUtil::CompileShader(L"Shadres\\color.hlsl", nullptr, "PS", "ps_5_1");
##6.7.1离线编译 略
##6.7.2 生成着色器代码 略
##6.7.3 利用Visual Studio离线编译着色器 Visual Studio 2015 集成了一些对着色器程序进行编译工做的支持,咱们能够向工程内添加hlsl文件,而Visual Studio会识别他们并提供编译的选项。可是,使用Visual Studio集成的HLSL工具备一个缺点,即它只容许每个文件中只能用一个着色器程序。所以,这个限制将致使顶点着色器和像素着色器不能同时放置在一个文件中,不然必有一个不会被编译。
##6.8 光栅器状态 在DriectX3D12的渲染流水线中,大多阶段都是能够编程的,可是有些特定阶段只接受配置,好比用于配置渲染流水线中光栅化阶段的光栅器状态组则由结构体D3D12_RASTERIZER_DESC表示
typedef struct D3D12_RASTERIZER_DESC { D3D12_FILL_MODE FillMode; //默认值为:D3D12_FILL_SOLID D3D12_CULL_MODE CullMode; //默认值为:D3D12_CULL_BACK BOOL FrontCounterClockwise; //默认值为:false INT DepthBias; //默认值为:0 FLOAT DepthBiasClamp; //默认值为:0.0f FLOAT SlopeScaleDepthBias; //默认值为:0.0f BOOL DepthClipEnable; //默认值为:true BOOL MultisampleEnable; //默认值为:false BOOL AntialiasedLineEnable; //默认值为:false UINT ForcedSampleCount; //默认值为:0 };
上面的结构体中大部分对咱们而言都是不怎么使用的成员,这里主要介绍三个:
- FileMode:用于指定是使用实体模式渲染仍是使用线框模式进行渲染
- CullMode:用于指定剔除模式,是使用背面剔除、正面剔除仍是不剔除
- FrontCounterClockwise:若是指定为false,则根据摄像机的观察视角,将顶点顺序为顺时针的视为正面朝向,若是为true,则根据将顶点顺序为逆时针的视为正面朝向
下列代码展现如何建立一个开启线框模式并且禁用剔除操做的光栅器状态:
CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT); rsDesc.FillMode = D3D12_FILL_MODE_WIREFRAME; rsDesc.CullMode = D3D12_CULL_MODE_NONE;
CD3DX12_RASTERIZER_DESC是扩展自D3D12_RASTERIZER_DESC结构体的基础上又添加了一些辅助构造函数的工具类,
##6.9 流水线状态对象 到目前为止,咱们已经展现了编写输入布局描述,建立顶点着色器和像素着色器,以及配置光栅器状态组这3个步骤,可是咱们尚未讲解如何将这些对象绑定到推向流水线上,用于绘制图形。流水线状态对象(Pipeline State Object,PSO)是控制大多数流水线状态对象的统称,ID3D12PipelineState接口表示,要建立PSO,首先咱们要填写一份描述其中细节的D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体实例:
typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC { ID3D12RootSignature * pRootSignature; //指向一个与该PSO绑定的根签名的指针 D3D12_SHADER_BYTECODE VS; //待绑定的顶点着色器 D3D12_SHADER_BYTECODE PS; //待绑定的像素着色器 D3D12_SHADER_BYTECODE DS; //待绑定的域着色器 D3D12_SHADER_BYTECODE HS; //待绑定的外壳着色器 D3D12_SHADER_BYTECODE GS; //待绑定的几何着色器 D3D12_STREAM_OUTPUT_DESC StreamOutput; //用于实现一种称为流输出的高级技术 D3D12_BLEND_DESC BlendState; //指定混合操做时所使用的混合状态 UINT SmapleMask; //设置每一个采样点的采集状况(采集或者禁止采集) D3D12_RASTERIZER_DESC RasterizerState; //指定用来配置光栅器的光栅器状态 D3D12_DEPTH_STENCIL_DESC DepthStencilState; //指定用于配置深度/模板测试的深度/模板状态 D3D12_INPUT_LAYOUT_DESC InputLayout; //输入布局描述 D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType; //指定图元的拓扑类型 UINT NumRenderTargets; //同时所用的渲染目标数量 DXGI_FORMAT RTVFormats[8]; //渲染目标的格式 DXGI_FORMAT DSVForamt; //深度/模板缓冲区的格式 DXGI_SAMPLE_DESC SmapleDesc; //描述多重采样对每个像素的采样数量以及质量级别 }D3D12_GRAPHICS_PIPELINE_STATE_DESC;
在D3D12_GRAPHICS_PIPELINE_DESC实例填写完毕以后,咱们即可以使用ID3D12Device::CreateGraphicsPipelineState方法来建立ID3D12PipelineState对象:
D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc; ZeroMemory(&PSODesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC)); PSODesc.InputLayout = { mInputLayout.data(),mInputLayout.size() }; PSODesc.pRootSignature = mRootSignature.Get(); PSODesc.VS = { reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()), mvsByteCode->GetBufferSize() }; PSODesc.PS = { reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), mpsByteCode->GetBufferSize() }; PSODesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); PSODesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); PSODesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); PSODesc.SampleMask = UINT_MAX; PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; PSODesc.RTVFormats[0] = mBackBufferFormat; PSODesc.SampleDesc.Count = m4xMsaaState ? 4 : 1; PSODesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; PSODesc.DSVFormat = mDepthStencilFormat; ComPtr<ID3D12PipelineState> mPSO; md3dDevice->CreateComputePipelineState(&PSODesc, IID_PPV_ARGS(&mPSO));
并不是全部的渲染状态都封装在PSO内,好比视口和裁剪矩形等属性就独立于PSO。Direct3D实质上就是一种状态机,里面的事物会保持它们各自的状态,直到咱们将他们改变。
##6.10 几何图形辅助结构体 通常来讲,咱们都会经过建立一个同时存有顶点缓冲区和索引缓冲区的结构体来方便的定义多个结构体,当须要定义多个结构体时,咱们就可使用定义在d3dUtil文件中的MeshGeometry结构体:
//先利用SubMeshGeometry来定义MeshGeometry中存储的单个结合体 //此结构体适用于将多个几何体数据存于一个顶点缓冲区和一个索引缓冲区的状况 struct SubmeshGeometry { UINT IndexCount = 0; UINT StartIndexLocaltion = 0; INT BaseVertexLoaction = 0; //经过此子网格来定义当前SubmeshGeometry结构体中所存结合体的包围盒(bounding box) DirectX::BoundingBox Bounds; }; struct MeshGeometry { //指定此几何体网格集合的名称,这样咱们就能根据名称找到它 std::string Name; //系统内存的副本,因为顶点/索引能够是泛型格式,因此用Blod类型表示 //待用户使用时再将他转换为适当的类型 Microsoft::WRL::ComPtr<ID3DBlob> VertexBufferCPU = nullptr; Microsoft::WRL::ComPtr<ID3DBlob> IndexBufferCPU = nullptr; Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr; Microsoft::WRL::ComPtr<ID3D12Resource> IndexBUfferGPU = nullptr; Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr; Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = nullptr; //与缓冲区相关的数据 UINT VertexByteStride = 0; UINT VertexBufferByteSize = 0; DXGI_FORMAT IndexForamt = DXGI_FORMAT_R16_UINT; UINT IndexBufferByteSize = 0; //一个MeshGeometry结构体可以存储一组顶点/索引缓冲区的多个几何体 //若利用下列容器阿里定义子网格几何体,咱们就能单独地绘制出其中的几何体 std::unordered_map<std::string, SubmeshGeometry> DrawArgs; //返回顶点缓冲区视图的方法 D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const { D3D12_VERTEX_BUFFER_VIEW vbv; vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress(); vbv.StrideInBytes = VertexByteStride; return vbv; } //返回索引缓冲区视图的方法 D3D12_INDEX_BUFFER_VIEW IndexBufferView()const { D3D12_INDEX_BUFFER_VIEW ibv; ibv.BufferLocation = IndexBUfferGPU->GetGPUVirtualAddress(); ibv.Format = IndexForamt; ibv.SizeInBytes = IndexBufferByteSize; return ibv; } //待数据上传到GPU后,咱们就能够释放这些内存了 void DisposeUploaders() { VertexBufferUploader = nullptr; IndexBufferUploader = nullptr; } };