为了让GPU可以获取vertex数组,需要将vertex存储在buffer(GPU资源ID3D12Resource)中,存储vertex的buffer称作vertex buffer。

 
 

要创建一个ID3D12Resource的话,需要填写一个用来描述buffer资源的结构体,这个结构体的名字叫做D3D12_RESOURCE_DESC

 
 

之后再调用ID3D12Device::CreateCommittedResource从而创建一个ID3D12Resource

 
 

 
 

而在DirectX12中提供了一个C++类:CD3DX12_RESOURCE_DESC

更详细的可以访问MSDN,会发现该类还提供了方便的构造其他类型资源的函数

A helper structure to enable easy initialization of a D3D12_RESOURCE_DESC structure.

来自 <https://docs.microsoft.com/en-us/windows/win32/direct3d12/cd3dx12-resource-desc>

 
 

该类派生自D3D12_RESOURCE_DESC,提供了方便的构造函数和方法。特别是提供了一个方法来简化填写描述缓冲区D3D12_RESOURCE_DESC结构体的构造函数:

static inline CD3DX12_RESOURCE_DESC Buffer

 

CD3DX12_RESOURCE_DESC static inline Buffer

(

UINT64 width,

D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,

UINT64 alignment = 0

);

对于缓冲区,width是指缓冲区中的字节数。例如,如果缓冲区存储了64个浮点,那么宽度将是64*sizeof(float)。

 
 

 
 

对于静态的几何体(每帧不更改的几何体)会存放在默认堆区D3D12_HEAP_TYPE_DEFAULT

对于不变动的几何体,比如树啊建筑啊这些东西,初始化顶点缓冲区后,只有GPU需要从顶点缓冲区读取数据来绘制几何体,因此默认堆是有意义的。默认堆区的存在可以提供客观的性能优势。

但是,CPU无法向默认堆区中的顶点缓冲区写入数据。如果不能写入数据,那么就没办法初始化一个顶点缓冲资源。不能初始化就意味着GPU没数据可以读取……

那么要怎样才能让CPU把数据写进堆区的顶点缓冲里面呢?其实看见D3D12_HEAP_TYPE_DEFAULT这个应该能想到type后面这个东西不止defualt,事实也确实如此:

typedef enum D3D12_HEAP_TYPE {

D3D12_HEAP_TYPE_DEFAULT,

D3D12_HEAP_TYPE_UPLOAD,

D3D12_HEAP_TYPE_READBACK,

D3D12_HEAP_TYPE_CUSTOM } ;

来自 <https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_heap_type>

可以看到有四个不同的类型,这是个枚举类型。其中第二个D3D12_HEAP_TYPE_UPLOAD是用来将数据从CPU复制到GPU的,船舰一个upload buffer,然后将vertex数据从系统内存复制到upload buffer,接下来就可以从upload buffer把数据复制到现行的vertex buffer。

但是如果想创建一个upload buffer,必须先初始化一个DEFAULT类型的buffer,在这里使用下面这个方法来简化工作,可以避免在需要创建defualt类型buffer的时候进行一些重复的工作:

static Microsoft::WRL::ComPtr<ID3D12Resource> CreateDefaultBuffer(

ID3D12Device* device,

ID3D12GraphicsCommandList* cmdList,

const void* initData,

UINT64 byteSize,

Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer);

具体的函数实现有点长,懒得调代码格式,可以直接看源码,这里我已经完善了一些注释

 
 

其中D3D12_SUBRESOURCE_DATA需要拿出来说一下,这个前面的章节没提过

typedef struct D3D12_SUBRESOURCE_DATA

{

const void *pData;

LONG_PTR RowPitch;

LONG_PTR SlicePitch;

}         D3D12_SUBRESOURCE_DATA;

参数:

  1. 一个指针,指向了系统内存里面存储了用来初始化buffer的数据的数组。如果buffer可以存储n个vertex,那么数组至少要有n个顶点才能初始化完整的buffer
  2. 对于buffer来说的,将要复制的数据的大小的字节数
  3. 同上

 
 

下面的代码展示了这个类如何被用来创建一个默认buffer来存储一个正方体的八个vertex,每个顶点都有不同的颜色:

Vertex vertices[] =

{

{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) },

{ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) },

{ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) },

{ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) },

{ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) },

{ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) },

{ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) },

{ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }

};//8个顶点的位置和颜色

 
 

const UINT64 vbByteSize = 8 * sizeof(Vertex);//8个顶点需要这么多的字节

 
 

ComPtr<ID3D12Resource> VertexBufferGPU = nullptr; //默认缓冲

ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;//upload缓冲区

 
 

VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(), mCommandList.Get(),vertices, vbByteSize, VertexBufferUploader);//调用前面说的方法

 
 

到此为止算是创建了一个vertex buffer,但是还需要将vertex buffer绑定到渲染管线上。想要绑定到渲染管线上需要创建一个vertex buffer view。

typedef struct D3D12_VERTEX_BUFFER_VIEW {

D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;

UINT SizeInBytes;

UINT StrideInBytes;

} D3D12_VERTEX_BUFFER_VIEW;

参数:

  1. 想要创建view的目标vertex buffer的虚拟地址
  2. 指明缓冲区有多少字节的大小
  3. 指明每个vertex元素有多少字节的大小

 
 

好了,创建完vertex buffer,再为这个buffer创建一个vertex buffer view,我们就可以将这个buffer绑定到渲染管线的一个input slot上,然后将vertex数据送到渲染管线的输入装配阶段。要实现这一步需要以下这个函数:

void IASetVertexBuffers(

UINT StartSlot,

UINT NumViews,

const D3D12_VERTEX_BUFFER_VIEW *pViews

);

来自 <https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-iasetvertexbuffers>

参数:

  1. 绑定vertex buffer的起始的input slot,一共有16个,从0开始。
  2. 绑定到input slot中的vertex buffer的数量,举例:如果第一个参数是n,那么绑定x个顶点缓冲,会依次绑定到n,n+1,n+2……n+x-1索引的input slot
  3. 指向存放vertex buffer view数组的第一个元素的指针。

下面是一个小例子:

D3D12_VERTEX_BUFFER_VIEW vbv;//定义一个view

vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();//获得了顶点缓冲的虚拟地址

vbv.StrideInBytes = sizeof(Vertex);//计算每个vertex占用的字节

vbv.SizeInBytes = 8 * sizeof(Vertex);//计算缓冲区总共有多少字节

D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = { vbv };//定义一个view的数组,将前面定义的view作为容器内元素进行初始化

mCommandList->IASetVertexBuffers(0, 1, vertexBuffers);//调用函数

 
 

IASetVertexBuffers是很好用的,它支持设置一整个数组的vertex buffer到不同的input slot,在这里只用一个。

一旦一个vertex buffer绑定到了一个input slot上,除非手动更改不然会一直保持绑定状态。所以如果要使用不止一个vertex buffer,可以按照如下方法编写代码:

ID3D12Resource* mVB1;//存储第一种类型的顶点数据

ID3D12Resource* mVB2;//存储另一种类型的顶点数据

D3D12_VERTEX_BUFFER_VIEW_DESC mVBView1;//为第一个vertex buffer创建的view

D3D12_VERTEX_BUFFER_VIEW_DESC mVBView2;//第二个view

/*创建vertex buffer和view*/

mCommandList->IASetVertexBuffers(0, 1, &mVBView1);//使用第一种顶点数据类型绘制

mCommandList->IASetVertexBuffers(0, 1, &mVBView2);//使用第二种绘制

 
 

将一个vertex buffer设置到input slot后并没有绘制,仅仅是将vertex数据准备好发送到渲染管线,最后一步实际进行绘制vertex是完成调用ID3D12GraphicsCommandList::DrawInstanced函数之后:

ID3D12GraphicsCommandList::DrawInstanced

void DrawInstanced(

UINT VertexCountPerInstance,

UINT InstanceCount,

UINT StartVertexLocation,

UINT StartInstanceLocation

);

来自 <https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-drawinstanced>

参数:

  1. 要绘制的vertex数量
  2. 用于一种高级特性,暂时用不到
  3. 指定第一个要绘制的vertex的索引,会从指定的这个开始绘制
  4. 同2,现在设置为0

 
 

第一个和第三个参数在vertex buffer中定义了一个连续的要绘制的vertex集合。


这个函数没有指明使用哪种装配方式,图元装配拓扑规则是函数ID3D12GraphicsCommandList::IASetPrimitiveTopology设置的。