为了使 GPU 可以访问顶点数组,就需要把它们放置在称为缓冲区 (buffer) 的 GPU 资源(ID3D12Resource) 里。我们把存储顶点的缓冲区叫作顶点缓冲区 (vertex buffer)。缓冲区的结构比纹理更为简单:既非多维资源,也不支持 mipmap、过滤器以及多重采样等技术。当需要向 GPU 提供如顶点这类数据元素所构成的数组时,我们便会使用缓冲区。
就像在创建深度/模板缓冲区中所做的那样,我们先通过填写 D3D12_RESOURCE_DESC 结构体来描述缓冲区资源,接着再调用 ID3D12Device::CreateCommittedResource 方法去创建ID3D12Resource 对象。至于 D3D12_RESOURCE_DESC 结构体中所有成员的介绍,可参考4.3.8节。Direct3D 12 提供了一个 C++ 包装类 CD3DX12_RESOURCE_DESC,它派生自D3D12_RESOURCE_DESC 结构体,并附有多种便于使用的构造函数以及方法。特别是它提供的下列方法,一种简化缓冲区描述过程的 D3D12_RESOURCE_DESC 的构造函数:
static inline CD3DX12_RESOURCE_DESC Buffer(
UINT64 width,
D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,
UINT64 alignment = 0 ){
return CD3DX12_RESOURCE_DESC( D3D12_RESOURCE_DIMENSION_BUFFER,
alignment, width, 1, 1, 1,
DXGI_FORMAT_UNKNOWN, 1, 0,
D3D12_TEXTURE_LAYOUT_ROW_MAJOR, flags );
}
对于缓冲区而言,函数代码中的 width 即表示缓冲区中所占的字节数。例如,若缓冲区存储了 64 个 float 类型的数据,那么 width 的值即为 64*sizeof(float)。
除此之外,CD3DX12_RESOURCE_DESC 类还提供了以下构建 D3D12_RESOURCE_DESC 结构体的简便方法,用于描述纹理资源及其可供查询的相关信息:
1.CD3DX12_RESOURCE_DESC::Tex1D
2.CD3DX12_RESOURCE_DESC::Tex2D
3.CD3DX12_RESOURCE_DESC::Tex3D
我们在第4章中曾提到深度/模板缓冲区,它是一种以 ID3D12Resource 对象表示的 2D 纹理。在Direct3D 12中,所有的资源均用 ID3D12Resource 接口表示。相比之下,Direct3D 11 则采用如ID3D11Buffer 与 ID3D11Texture2D 等多种不同的接口来表示各种不同的资源。而且,在 Direct3D 12 中,资源的类型由 D3D12_RESOURCE_DESC::D3D12_RESOURCE_DIMENSION 字段来加以区分。
例如:
缓冲区用 D3D12_RESOURCE_DIMENSION_BUFFER 类型表示。
2D 纹理用 D3D12_RESOURCE_DIMENSION_TEXTURE2D 类型表示。
对于静态几何体 (static geometry,即每一帧都不会发生改变的几何体,如游戏中的树木、建筑物、地形和角色动画) 而言,我们会将其顶点缓冲区置于默认堆 (D3D12_HEAP_TYPE_DEFAULT) 中来优化性能。在这种情况下,顶点缓冲区初始化完毕之后,只有 GPU 需要从其中读取数据来绘制几何体。
如果 CPU 不能向默认堆中的顶点缓冲区写入数据,那么如何初始化此顶点缓冲区:
除了创建顶点缓冲区资源本身之外,还需用 D3D12_HEAP_TYPE_UPLOAD 这种堆类型来创建中介类型的上传缓冲区 (upload buffer) 资源。在4.3.8节里,我们就是通过把资源提交至上传堆,才得以将数据从 CPU 复制到 GPU 显存中。在创建了上传缓冲区之后,我们就可以将顶点数据从系统内存复制到上传缓冲区,而后再把顶点数据从上传缓冲区复制到真正的顶点缓冲区中。
3D12 的 ID3D12Heap 分为以下几种类型:
D3D12_HEAP_TYPE_DEFAULT,同 Vulkan 的 DEVICE_LOCAL 类似,这是一种 GPU 访问最快的 Heap 类型。但不允许 CPU 通过 Mapping 直接访问。
D3D12_HEAP_TYPE_UPLOAD ,这是一种可以由 CPU 访问的系统内存,可以通过 Mapping 访问数据。一般用于上传数据到在 D3D12_HEAP_TYPE_DEFAULT 堆上创建的资源。
D3D12_HEAP_TYPE_READBACK,这是一种可以由 GPU 写入数据,CPU 回读数据的系统内存,此堆类型最适合 GPU 一次写入,CPU 可读的数据。
由于我们需要利用作为中介的上传缓冲区来初始化默认缓冲区(即用堆类型D3D12_HEAP_TYPE_DEFAULT 创建的缓冲区)中的数据,因此,我们就在 d3dUtil.h/.cpp 文件中构建了下列工具函数。
D3D12_SUBRESORSE_DATA 结构体的定义:
typedef struct D3D12_SUBRESOURCE_DATA
{
const void *pData;
LONG_PTR RowPitch;
LONG_PTR SlicePitch;
} D3D12_SUBRESOURCE_DATA;
1. pData:指向某个系统内存块的指针,其中有初始化缓冲区所用的数据。如果欲初始化的缓冲区能够存储 n 个顶点数据,则该系统内存块必定可容纳至少 n 个顶点数据,以此来初始化整个缓冲区。
2. RowPitch 和 SlicePitch:对于缓冲区而言,此参数为欲复制数据的字节数。
下面的代码演示了此类将如何创建存有立方体8个顶点的默认缓冲区,并为其中的每个顶点都分别赋予了不同的颜色。
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) }
};
const UINT64 vbByteSize = 8 * sizeof(Vertex);
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
代码中的 Vertex 类型(即顶点的坐标及其颜色)的定义如下。
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
为了将顶点缓冲区绑定到渲染流水线上,我们需要给这种资源创建一个顶点缓冲区视图 (vertex buffer view)。与 RTV (渲染目标视图) 不同的是,我们无须为顶点缓冲区视图创建描述符堆。而且,顶点缓冲区视图是由 D3D12_VERTEX_BUFFER_VIEW 结构体来表示。
typedef struct D3D12_VERTEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
1. BufferLocation:待创建视图的顶点缓冲区资源虚拟地址。我们可以通过 ID3D12Resource::GetGPUVirtualAddress 方法来获得此地址。
2. SizeInBytes:待创建视图的顶点缓冲区大小(用字节表示)。
3. StrideInBytes:每个顶点元素所占用的字节数。
在顶点缓冲区及其对应视图创建完成后,便可以将它与渲染流水线上的一个输入槽 (input slot) 相绑定。这样一来,我们就能向流水线中的输入装配器阶段传递顶点数据了。此操作可以通过下列方法来实现。
void ID3D12GraphicsCommandList::IASetVertexBuffers(
UINT StartSlot,
UINT NumView,
const D3D12_VERTEX_BUFFER_VIEW *pViews);
1. StartSlot:在绑定多个顶点缓冲区时,所用的起始输入槽(若仅有一个顶点缓冲区,则将其绑定至此槽)。输入槽共有16个,索引为0~15。
2. NumViews:将要与输入槽绑定的顶点缓冲区数量(即视图数组 pViews 中视图的数量)。如果起始输入槽 StartSlot 的索引值为 k,且我们要绑定 n 个顶点缓冲区,那么这些缓冲区将依次与输入槽 ,,..., 相绑定。
3. pViews:指向顶点缓冲区视图数组中第一个元素的指针。
该函数的调用示例:
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);
D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = { vbv };
mCommandList->IASetVertexBuffers(0, 1, vertexBuffers);
由于 IASetVertexBuffers 方法会将顶点缓冲区数组中的元素设置到不同的输入槽上去,所以使它看起来似乎有些复杂。但是,在我们的示例中实际只会使用一个输入槽。
我们若不对顶点缓冲区进行任何修改,它就将一直被绑定于所在的输入槽上。所以,如果使用多个顶点缓冲区,那么就可以按以下流程来构建代码:
ID3D12Resource* mVB1; // 存储Vertex1类型的顶点
ID3D12Resource* mVB2; // 存储Vertex2类型的顶点
D3D12_VERTEX_BUFFER_VIEW mVBView1; // mVB1的视图
D3D12_VERTEX_BUFFER_VIEW mVBView2; // mVB2的视图
/*……创建顶点缓冲区及其视图……*/
mCommandList->IASetVertexBuffers(0, 1, &mVBView1);/* ……使用顶点缓冲区1来绘制物体…… */
mCommandList->IASetVertexBuffers(0, 1, &mVBView2);/* ……使用顶点缓冲区2来绘制物体…… */
将顶点缓冲区设置到输入槽上并不会对其执行实际的绘制操作,而是仅为顶点数据送至渲染流水线做好准备而已。这最后一步才是通过 ID3D12GraphicsCommandList::DrawInstanced 方法真正地绘制顶点:
void ID3D12GraphicsCommandList::DrawInstanced (
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation);
1. VertexCountPerInstance:每个实例要绘制的顶点数量。
2. InstanceCount:启用实例化 (instancing) 高级技术。就目前来说,我们只绘制一个实例,因而将此参数设置为 1。
3. StartVertexLocation:指定顶点缓冲区内第一个被绘制顶点的索引(该索引值以 0 为基准)。
4. StartInstanceLocation:实例化 (instancing) 相关,暂时只需将其设置为 0。
VertexCountPerInstance 和 StartVertexLocation 两个参数定义了顶点缓冲区中将要被绘制的一组连续顶点,如图所示:
StartVertexLocation 参数指定了顶点缓冲区中第一个被绘制顶点的索引(此索引从 0 开始计),VertexCountPerInstance 指定了欲绘制顶点的个数
既然 DrawInstanced 方法没有指定顶点被定义为何种图元,那么,它们应该被绘制为点、线列表还是三角形列表呢?回顾 5.5.2 节可知,图元拓扑状态实由ID3D12GraphicsCommandList::IASetPrimitiveTopology 方法来设置。下面给出一个相关的调用示例:
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);